From 9efe4b3c4a5f07c45cf6ed69900006eed0e844b9 Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Mon, 9 May 2022 18:16:15 -0400 Subject: [PATCH] init (#1) * init * some copy pastin * install.sh ref * fixes --- collection/anaconda/feature.json | 16 + collection/anaconda/install.sh | 99 ++++ collection/aws-cli/feature.json | 21 + collection/aws-cli/install.sh | 142 ++++++ collection/azure-cli/feature.json | 17 + collection/azure-cli/install.sh | 186 +++++++ collection/common/feature.json | 52 ++ collection/common/install.sh | 454 ++++++++++++++++++ collection/desktop-lite/feature.json | 39 ++ collection/desktop-lite/install.sh | 394 +++++++++++++++ collection/docker-from-docker/feature.json | 41 ++ collection/docker-from-docker/install.sh | 354 ++++++++++++++ collection/docker-in-docker/feature.json | 42 ++ collection/docker-in-docker/install.sh | 397 +++++++++++++++ collection/dotnet/feature.json | 28 ++ collection/dotnet/install.sh | 374 +++++++++++++++ collection/git-lfs/feature.json | 16 + collection/git-lfs/install.sh | 194 ++++++++ collection/git/feature.json | 21 + collection/git/install.sh | 153 ++++++ collection/github-cli/feature.json | 16 + collection/github-cli/install.sh | 188 ++++++++ collection/go/feature.json | 24 + collection/go/install.sh | 255 ++++++++++ collection/gradle/feature.json | 22 + collection/gradle/install.sh | 0 collection/hugo/feature.json | 16 + collection/hugo/install.sh | 113 +++++ collection/java/feature.json | 26 + collection/java/install.sh | 145 ++++++ collection/jekyll/feature.json | 16 + collection/jekyll/install.sh | 60 +++ collection/jupyterlab/feature.json | 21 + collection/jupyterlab/install.sh | 51 ++ collection/kubectl-helm-minikube/feature.json | 38 ++ collection/kubectl-helm-minikube/install.sh | 249 ++++++++++ collection/maven/feature.json | 22 + collection/maven/install.sh | 133 +++++ collection/node/feature.json | 27 ++ collection/node/install.sh | 169 +++++++ collection/powershell/feature.json | 16 + collection/powershell/install.sh | 170 +++++++ collection/python/feature.json | 56 +++ collection/python/python-debian.sh | 376 +++++++++++++++ collection/ruby/feature.json | 25 + collection/ruby/install.sh | 271 +++++++++++ collection/rust/feature.json | 43 ++ collection/rust/install.sh | 199 ++++++++ collection/sshd/feature.json | 17 + collection/sshd/install.sh | 169 +++++++ collection/terraform/feature.json | 39 ++ collection/terraform/install.sh | 218 +++++++++ lib/utils.sh | 244 ++++++++++ settings.env | 56 +++ 54 files changed, 6530 insertions(+) create mode 100644 collection/anaconda/feature.json create mode 100644 collection/anaconda/install.sh create mode 100644 collection/aws-cli/feature.json create mode 100644 collection/aws-cli/install.sh create mode 100644 collection/azure-cli/feature.json create mode 100644 collection/azure-cli/install.sh create mode 100644 collection/common/feature.json create mode 100644 collection/common/install.sh create mode 100644 collection/desktop-lite/feature.json create mode 100644 collection/desktop-lite/install.sh create mode 100644 collection/docker-from-docker/feature.json create mode 100644 collection/docker-from-docker/install.sh create mode 100644 collection/docker-in-docker/feature.json create mode 100644 collection/docker-in-docker/install.sh create mode 100644 collection/dotnet/feature.json create mode 100644 collection/dotnet/install.sh create mode 100644 collection/git-lfs/feature.json create mode 100644 collection/git-lfs/install.sh create mode 100644 collection/git/feature.json create mode 100644 collection/git/install.sh create mode 100644 collection/github-cli/feature.json create mode 100644 collection/github-cli/install.sh create mode 100644 collection/go/feature.json create mode 100644 collection/go/install.sh create mode 100644 collection/gradle/feature.json create mode 100644 collection/gradle/install.sh create mode 100644 collection/hugo/feature.json create mode 100644 collection/hugo/install.sh create mode 100644 collection/java/feature.json create mode 100644 collection/java/install.sh create mode 100644 collection/jekyll/feature.json create mode 100644 collection/jekyll/install.sh create mode 100644 collection/jupyterlab/feature.json create mode 100644 collection/jupyterlab/install.sh create mode 100644 collection/kubectl-helm-minikube/feature.json create mode 100644 collection/kubectl-helm-minikube/install.sh create mode 100644 collection/maven/feature.json create mode 100644 collection/maven/install.sh create mode 100644 collection/node/feature.json create mode 100644 collection/node/install.sh create mode 100644 collection/powershell/feature.json create mode 100644 collection/powershell/install.sh create mode 100644 collection/python/feature.json create mode 100644 collection/python/python-debian.sh create mode 100644 collection/ruby/feature.json create mode 100644 collection/ruby/install.sh create mode 100644 collection/rust/feature.json create mode 100644 collection/rust/install.sh create mode 100644 collection/sshd/feature.json create mode 100644 collection/sshd/install.sh create mode 100644 collection/terraform/feature.json create mode 100644 collection/terraform/install.sh create mode 100644 lib/utils.sh create mode 100644 settings.env diff --git a/collection/anaconda/feature.json b/collection/anaconda/feature.json new file mode 100644 index 0000000..7af90cd --- /dev/null +++ b/collection/anaconda/feature.json @@ -0,0 +1,16 @@ +{ + "id": "anaconda", + "name": "Anaconda", + "options": { + "version": { + "type": "string", + "proposals": ["latest"], + "default": "latest", + "description": "Select or enter an anaconda version." + } + }, + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/anaconda/install.sh b/collection/anaconda/install.sh new file mode 100644 index 0000000..c77d98d --- /dev/null +++ b/collection/anaconda/install.sh @@ -0,0 +1,99 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/anaconda.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./anaconda-debian.sh [Conda version] [CONDA_DIR] [Non-root user] [Add rc files flag] + +VERSION=${1:-"latest"} +export CONDA_DIR=${2:-"/usr/local/conda"} +USERNAME=${3:-"automatic"} +UPDATE_RC=${4:-"true"} + +set -eux +export DEBIAN_FRONTEND=noninteractive + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +architecture="$(uname -m)" +if [ "${architecture}" != "x86_64" ]; then + echo "(!) Architecture $architecture unsupported" + exit 1 +fi + +updaterc() { + if [ "${UPDATE_RC}" = "true" ]; then + echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." + if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/bash.bashrc + fi + if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/zsh/zshrc + fi + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt-get update + apt-get -y install --no-install-recommends "$@" + fi +} + +# Install Conda if it's missing +if ! conda --version &> /dev/null ; then + # Install dependencies + check_packages wget ca-certificates + + mkdir -p $CONDA_DIR + chown ${USERNAME}:root $CONDA_DIR + echo "Installing Anaconda..." + + CONDA_VERSION=$VERSION + if [ "${VERSION}" = "latest" ] || [ "${VERSION}" = "lts" ]; then + CONDA_VERSION="2021.11" + fi + + su --login -c "wget -q https://repo.anaconda.com/archive/Anaconda3-${CONDA_VERSION}-Linux-x86_64.sh -O /tmp/anaconda-install.sh \ + && /bin/bash /tmp/anaconda-install.sh -u -b -p ${CONDA_DIR}" ${USERNAME} 2>&1 + + if [ "${VERSION}" = "latest" ] || [ "${VERSION}" = "lts" ]; then + PATH=$PATH:${CONDA_DIR}/bin + conda update -y conda + fi + + rm /tmp/anaconda-install.sh + updaterc "export CONDA_DIR=${CONDA_DIR}/bin" +fi + +echo "Done!" \ No newline at end of file diff --git a/collection/aws-cli/feature.json b/collection/aws-cli/feature.json new file mode 100644 index 0000000..ee1df10 --- /dev/null +++ b/collection/aws-cli/feature.json @@ -0,0 +1,21 @@ +{ + "id": "aws-cli", + "name": "AWS CLI", + "options": { + "version": { + "type": "string", + "proposals": [ + "latest" + ], + "default": "latest", + "description": "Select or enter an AWS CLI version. (Available versions here: https://github.com/aws/aws-cli/blob/v2/CHANGELOG.rst)" + } + }, + "extensions": [ + "AmazonWebServices.aws-toolkit-vscode" + ], + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/aws-cli/install.sh b/collection/aws-cli/install.sh new file mode 100644 index 0000000..0d204de --- /dev/null +++ b/collection/aws-cli/install.sh @@ -0,0 +1,142 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/awscli.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./awscli-debian.sh [AWS CLI version] + +set -e + +AWSCLI_VERSION=${1:-"latest"} +AWSCLI_GPG_KEY=FB5DB77FD5C118B80511ADA8A6310ACC4672475C +AWSCLI_GPG_KEY_MATERIAL="-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBF2Cr7UBEADJZHcgusOJl7ENSyumXh85z0TRV0xJorM2B/JL0kHOyigQluUG +ZMLhENaG0bYatdrKP+3H91lvK050pXwnO/R7fB/FSTouki4ciIx5OuLlnJZIxSzx +PqGl0mkxImLNbGWoi6Lto0LYxqHN2iQtzlwTVmq9733zd3XfcXrZ3+LblHAgEt5G +TfNxEKJ8soPLyWmwDH6HWCnjZ/aIQRBTIQ05uVeEoYxSh6wOai7ss/KveoSNBbYz +gbdzoqI2Y8cgH2nbfgp3DSasaLZEdCSsIsK1u05CinE7k2qZ7KgKAUIcT/cR/grk +C6VwsnDU0OUCideXcQ8WeHutqvgZH1JgKDbznoIzeQHJD238GEu+eKhRHcz8/jeG +94zkcgJOz3KbZGYMiTh277Fvj9zzvZsbMBCedV1BTg3TqgvdX4bdkhf5cH+7NtWO +lrFj6UwAsGukBTAOxC0l/dnSmZhJ7Z1KmEWilro/gOrjtOxqRQutlIqG22TaqoPG +fYVN+en3Zwbt97kcgZDwqbuykNt64oZWc4XKCa3mprEGC3IbJTBFqglXmZ7l9ywG +EEUJYOlb2XrSuPWml39beWdKM8kzr1OjnlOm6+lpTRCBfo0wa9F8YZRhHPAkwKkX +XDeOGpWRj4ohOx0d2GWkyV5xyN14p2tQOCdOODmz80yUTgRpPVQUtOEhXQARAQAB +tCFBV1MgQ0xJIFRlYW0gPGF3cy1jbGlAYW1hem9uLmNvbT6JAlQEEwEIAD4WIQT7 +Xbd/1cEYuAURraimMQrMRnJHXAUCXYKvtQIbAwUJB4TOAAULCQgHAgYVCgkICwIE +FgIDAQIeAQIXgAAKCRCmMQrMRnJHXJIXEAChLUIkg80uPUkGjE3jejvQSA1aWuAM +yzy6fdpdlRUz6M6nmsUhOExjVIvibEJpzK5mhuSZ4lb0vJ2ZUPgCv4zs2nBd7BGJ +MxKiWgBReGvTdqZ0SzyYH4PYCJSE732x/Fw9hfnh1dMTXNcrQXzwOmmFNNegG0Ox +au+VnpcR5Kz3smiTrIwZbRudo1ijhCYPQ7t5CMp9kjC6bObvy1hSIg2xNbMAN/Do +ikebAl36uA6Y/Uczjj3GxZW4ZWeFirMidKbtqvUz2y0UFszobjiBSqZZHCreC34B +hw9bFNpuWC/0SrXgohdsc6vK50pDGdV5kM2qo9tMQ/izsAwTh/d/GzZv8H4lV9eO +tEis+EpR497PaxKKh9tJf0N6Q1YLRHof5xePZtOIlS3gfvsH5hXA3HJ9yIxb8T0H +QYmVr3aIUes20i6meI3fuV36VFupwfrTKaL7VXnsrK2fq5cRvyJLNzXucg0WAjPF +RrAGLzY7nP1xeg1a0aeP+pdsqjqlPJom8OCWc1+6DWbg0jsC74WoesAqgBItODMB +rsal1y/q+bPzpsnWjzHV8+1/EtZmSc8ZUGSJOPkfC7hObnfkl18h+1QtKTjZme4d +H17gsBJr+opwJw/Zio2LMjQBOqlm3K1A4zFTh7wBC7He6KPQea1p2XAMgtvATtNe +YLZATHZKTJyiqA== +=vYOk +-----END PGP PUBLIC KEY BLOCK-----" + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +export DEBIAN_FRONTEND=noninteractive + +check_packages curl ca-certificates gnupg2 dirmngr unzip + +verify_aws_cli_gpg_signature() { + local filePath=$1 + local sigFilePath=$2 + + get_common_setting AWSCLI_GPG_KEY + get_common_setting AWSCLI_GPG_KEY_MATERIAL true + local awsGpgKeyring=aws-cli-public-key.gpg + + echo "${AWSCLI_GPG_KEY_MATERIAL}" | gpg --dearmor > "./${awsGpgKeyring}" + gpg --batch --quiet --no-default-keyring --keyring "./${awsGpgKeyring}" --verify "${sigFilePath}" "${filePath}" + local status=$? + + rm "./${awsGpgKeyring}" + + return ${status} +} + +install() { + local scriptZipFile=awscli.zip + local scriptSigFile=awscli.sig + + # See Linux install docs at https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html + if [ "${AWSCLI_VERSION}" != "latest" ]; then + local versionStr=-${AWSCLI_VERSION} + fi + architecture=$(dpkg --print-architecture) + case "${architecture}" in + amd64) architectureStr=x86_64 ;; + arm64) architectureStr=aarch64 ;; + *) + echo "AWS CLI does not support machine architecture '$architecture'. Please use an x86-64 or ARM64 machine." + exit 1 + esac + local scriptUrl=https://awscli.amazonaws.com/awscli-exe-linux-${architectureStr}${versionStr}.zip + curl "${scriptUrl}" -o "${scriptZipFile}" + curl "${scriptUrl}.sig" -o "${scriptSigFile}" + + verify_aws_cli_gpg_signature "$scriptZipFile" "$scriptSigFile" + if (( $? > 0 )); then + echo "Could not verify GPG signature of AWS CLI install script. Make sure you provided a valid version." + exit 1 + fi + + unzip "${scriptZipFile}" + ./aws/install + + rm -rf ./aws +} + +echo "(*) Installing AWS CLI..." + +install + +echo "Done!" diff --git a/collection/azure-cli/feature.json b/collection/azure-cli/feature.json new file mode 100644 index 0000000..6ae3769 --- /dev/null +++ b/collection/azure-cli/feature.json @@ -0,0 +1,17 @@ +{ + "id": "azure-cli", + "name": "Azure CLI", + "options": { + "version": { + "type": "string", + "proposals": ["latest"], + "default": "latest", + "description": "Select or enter an Azure CLI version. (Available versions may vary by Linux distribution.)" + } + }, + "extensions": ["ms-vscode.azurecli"], + "install": { + "app": "", + "file": "install.sh" + } +} diff --git a/collection/azure-cli/install.sh b/collection/azure-cli/install.sh new file mode 100644 index 0000000..a7f2a84 --- /dev/null +++ b/collection/azure-cli/install.sh @@ -0,0 +1,186 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/azcli.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./azcli-debian.sh + +set -e + +AZ_VERSION=${1:-"latest"} +MICROSOFT_GPG_KEYS_URI="https://packages.microsoft.com/keys/microsoft.asc" +AZCLI_ARCHIVE_ARCHITECTURES="amd64" +AZCLI_ARCHIVE_VERSION_CODENAMES="stretch buster bullseye bionic focal" + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +export DEBIAN_FRONTEND=noninteractive + +# Soft version matching that resolves a version for a given package in the *current apt-cache* +# Return value is stored in first argument (the unprocessed version) +apt_cache_version_soft_match() { + + # Version + local variable_name="$1" + local requested_version=${!variable_name} + # Package Name + local package_name="$2" + # Exit on no match? + local exit_on_no_match="${3:-true}" + + # Ensure we've exported useful variables + . /etc/os-release + local architecture="$(dpkg --print-architecture)" + + dot_escaped="${requested_version//./\\.}" + dot_plus_escaped="${dot_escaped//+/\\+}" + # Regex needs to handle debian package version number format: https://www.systutorials.com/docs/linux/man/5-deb-version/ + version_regex="^(.+:)?${dot_plus_escaped}([\\.\\+ ~:-]|$)" + set +e # Don't exit if finding version fails - handle gracefully + fuzzy_version="$(apt-cache madison ${package_name} | awk -F"|" '{print $2}' | sed -e 's/^[ \t]*//' | grep -E -m 1 "${version_regex}")" + set -e + if [ -z "${fuzzy_version}" ]; then + echo "(!) No full or partial for package \"${package_name}\" match found in apt-cache for \"${requested_version}\" on OS ${ID} ${VERSION_CODENAME} (${architecture})." + + if $exit_on_no_match; then + echo "Available versions:" + apt-cache madison ${package_name} | awk -F"|" '{print $2}' | grep -oP '^(.+:)?\K.+' + exit 1 # Fail entire script + else + echo "Continuing to fallback method (if available)" + return 1; + fi + fi + + # Globally assign fuzzy_version to this value + # Use this value as the return value of this function + declare -g ${variable_name}="=${fuzzy_version}" + echo "${variable_name} ${!variable_name}" +} + +install_using_apt() { + # Install dependencies + check_packages apt-transport-https curl ca-certificates gnupg2 dirmngr + # Import key safely (new 'signed-by' method rather than deprecated apt-key approach) and install + get_common_setting MICROSOFT_GPG_KEYS_URI + curl -sSL ${MICROSOFT_GPG_KEYS_URI} | gpg --dearmor > /usr/share/keyrings/microsoft-archive-keyring.gpg + echo "deb [arch=${architecture} signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/repos/azure-cli/ ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/azure-cli.list + apt-get update + + if [ "${AZ_VERSION}" = "latest" ] || [ "${AZ_VERSION}" = "lts" ] || [ "${AZ_VERSION}" = "stable" ]; then + # Empty, meaning grab the "latest" in the apt repo + AZ_VERSION="" + else + # Sets AZ_VERSION to our desired version, if match found. + apt_cache_version_soft_match AZ_VERSION "azure-cli" false + if [ "$?" != 0 ]; then + return 1 + fi + fi + + if ! (apt-get install -yq azure-cli${AZ_VERSION}); then + rm -f /etc/apt/sources.list.d/azure-cli.list + return 1 + fi +} + +install_using_pip() { + echo "(*) No pre-built binaries available in apt-cache. Installing via pip3." + if ! dpkg -s python3-minimal python3-pip libffi-dev python3-venv > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install python3-minimal python3-pip libffi-dev python3-venv + fi + export PIPX_HOME=/usr/local/pipx + mkdir -p ${PIPX_HOME} + export PIPX_BIN_DIR=/usr/local/bin + export PYTHONUSERBASE=/tmp/pip-tmp + export PIP_CACHE_DIR=/tmp/pip-tmp/cache + pipx_bin=pipx + if ! type pipx > /dev/null 2>&1; then + pip3 install --disable-pip-version-check --no-cache-dir --user pipx + pipx_bin=/tmp/pip-tmp/bin/pipx + fi + + if [ "${AZ_VERSION}" = "latest" ] || [ "${AZ_VERSION}" = "lts" ] || [ "${AZ_VERSION}" = "stable" ]; then + # Empty, meaning grab the "latest" in the apt repo + ver="" + else + ver="==${AZ_VERSION}" + fi + + set +e + ${pipx_bin} install --pip-args '--no-cache-dir --force-reinstall' -f azure-cli${ver} + + # Fail gracefully + if [ "$?" != 0 ]; then + echo "Could not install azure-cli${ver} via pip" + rm -rf /tmp/pip-tmp + return 1 + fi + set -e +} + +# See if we're on x86_64 and if so, install via apt-get, otherwise use pip3 +echo "(*) Installing Azure CLI..." +. /etc/os-release +architecture="$(dpkg --print-architecture)" +if [[ "${AZCLI_ARCHIVE_ARCHITECTURES}" = *"${architecture}"* ]] && [[ "${AZCLI_ARCHIVE_VERSION_CODENAMES}" = *"${VERSION_CODENAME}"* ]]; then + install_using_apt || use_pip="true" +else + use_pip="true" +fi + +if [ "${use_pip}" = "true" ]; then + install_using_pip + + if [ "$?" != 0 ]; then + echo "Please provide a valid version for your distribution ${ID} ${VERSION_CODENAME} (${architecture})." + echo + echo "Valid versions in current apt-cache" + apt-cache madison azure-cli | awk -F"|" '{print $2}' | grep -oP '^(.+:)?\K.+' + exit 1 + fi +fi + +echo "Done!" \ No newline at end of file diff --git a/collection/common/feature.json b/collection/common/feature.json new file mode 100644 index 0000000..d6b94f2 --- /dev/null +++ b/collection/common/feature.json @@ -0,0 +1,52 @@ +{ + "id": "common", + "name": "common", + "description": "common", + "options": { + "installZsh": { + "type": "boolean", + "default": true, + "description": "Install ZSH?" + }, + "installOhMyZsh": { + "type": "boolean", + "default": true, + "description": "Install Oh My Zsh!?" + }, + "upgradePackages": { + "type":"boolean", + "default": true, + "description": "Upgrade OS packages?" + }, + "username": { + "type":"string", + "proposals": ["vscode", "codespace", "none", "automatic"], + "default": "automatic", + "description": "Enter name of non-root user to configure or none to skip" + }, + "uid": { + "type":"string", + "proposals": ["1000", "automatic"], + "default": "automatic", + "description": "Enter uid for non-root user" + }, + "gid": { + "type": "string", + "proposals": ["1000", "automatic"], + "default": "automatic", + "description": "Enter gid for non-root user" + }, + "nonFreePackages": { + "type":"boolean", + "default": true, + "description": "Add packages from non-free Debian repository?" + } + }, + "extensions": [ + "ms-dotnettools.csharp" + ], + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/common/install.sh b/collection/common/install.sh new file mode 100644 index 0000000..af4facc --- /dev/null +++ b/collection/common/install.sh @@ -0,0 +1,454 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/common.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My Zsh! flag] [Add non-free packages] + +set -e + +INSTALL_ZSH=${1:-"true"} +USERNAME=${2:-"automatic"} +USER_UID=${3:-"automatic"} +USER_GID=${4:-"automatic"} +UPGRADE_PACKAGES=${5:-"true"} +INSTALL_OH_MYS=${6:-"true"} +ADD_NON_FREE_PACKAGES=${7:-"false"} +SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" +MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# If in automatic mode, determine if a user already exists, if not use vscode +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=vscode + fi +elif [ "${USERNAME}" = "none" ]; then + USERNAME=root + USER_UID=0 + USER_GID=0 +fi + +# Load markers to see which steps have already run +if [ -f "${MARKER_FILE}" ]; then + echo "Marker file found:" + cat "${MARKER_FILE}" + source "${MARKER_FILE}" +fi + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Function to call apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies +if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then + + package_list="apt-utils \ + openssh-client \ + gnupg2 \ + dirmngr \ + iproute2 \ + procps \ + lsof \ + htop \ + net-tools \ + psmisc \ + curl \ + wget \ + rsync \ + ca-certificates \ + unzip \ + zip \ + nano \ + vim-tiny \ + less \ + jq \ + lsb-release \ + apt-transport-https \ + dialog \ + libc6 \ + libgcc1 \ + libkrb5-3 \ + libgssapi-krb5-2 \ + libicu[0-9][0-9] \ + liblttng-ust0 \ + libstdc++6 \ + zlib1g \ + locales \ + sudo \ + ncdu \ + man-db \ + strace \ + manpages \ + manpages-dev \ + init-system-helpers" + + # Needed for adding manpages-posix and manpages-posix-dev which are non-free packages in Debian + if [ "${ADD_NON_FREE_PACKAGES}" = "true" ]; then + # Bring in variables from /etc/os-release like VERSION_CODENAME + . /etc/os-release + sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb-src http:\/\/(deb|httredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb-src http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list + # Handle bullseye location for security https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html + sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list + echo "Running apt-get update..." + apt-get update + package_list="${package_list} manpages-posix manpages-posix-dev" + else + apt_get_update_if_needed + fi + + # Install libssl1.1 if available + if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then + package_list="${package_list} libssl1.1" + fi + + # Install appropriate version of libssl1.0.x if available + libssl_package=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') + if [ "$(echo "$LIlibssl_packageBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then + if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then + # Debian 9 + package_list="${package_list} libssl1.0.2" + elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then + # Ubuntu 18.04, 16.04, earlier + package_list="${package_list} libssl1.0.0" + fi + fi + + echo "Packages to verify are installed: ${package_list}" + apt-get -y install --no-install-recommends ${package_list} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) + + # Install git if not already installed (may be more recent than distro version) + if ! type git > /dev/null 2>&1; then + apt-get -y install --no-install-recommends git + fi + + PACKAGES_ALREADY_INSTALLED="true" +fi + +# Get to latest versions of all packages +if [ "${UPGRADE_PACKAGES}" = "true" ]; then + apt_get_update_if_needed + apt-get -y upgrade --no-install-recommends + apt-get autoremove -y +fi + +# Ensure at least the en_US.UTF-8 UTF-8 locale is available. +# Common need for both applications and things like the agnoster ZSH theme. +if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then + echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen + locale-gen + LOCALE_ALREADY_SET="true" +fi + +# Create or update a non-root user to match UID/GID. +group_name="${USERNAME}" +if id -u ${USERNAME} > /dev/null 2>&1; then + # User exists, update if needed + if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -g $USERNAME)" ]; then + group_name="$(id -gn $USERNAME)" + groupmod --gid $USER_GID ${group_name} + usermod --gid $USER_GID $USERNAME + fi + if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then + usermod --uid $USER_UID $USERNAME + fi +else + # Create user + if [ "${USER_GID}" = "automatic" ]; then + groupadd $USERNAME + else + groupadd --gid $USER_GID $USERNAME + fi + if [ "${USER_UID}" = "automatic" ]; then + useradd -s /bin/bash --gid $USERNAME -m $USERNAME + else + useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME + fi +fi + +# Add add sudo support for non-root user +if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then + echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME + chmod 0440 /etc/sudoers.d/$USERNAME + EXISTING_NON_ROOT_USER="${USERNAME}" +fi + +# ** Shell customization section ** +if [ "${USERNAME}" = "root" ]; then + user_rc_path="/root" +else + user_rc_path="/home/${USERNAME}" +fi + +# Restore user .bashrc defaults from skeleton file if it doesn't exist or is empty +if [ ! -f "${user_rc_path}/.bashrc" ] || [ ! -s "${user_rc_path}/.bashrc" ] ; then + cp /etc/skel/.bashrc "${user_rc_path}/.bashrc" +fi + +# Restore user .profile defaults from skeleton file if it doesn't exist or is empty +if [ ! -f "${user_rc_path}/.profile" ] || [ ! -s "${user_rc_path}/.profile" ] ; then + cp /etc/skel/.profile "${user_rc_path}/.profile" +fi + +# .bashrc/.zshrc snippet +rc_snippet="$(cat << 'EOF' + +if [ -z "${USER}" ]; then export USER=$(whoami); fi +if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi + +# Display optional first run image specific notice if configured and terminal is interactive +if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then + if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then + cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" + elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then + cat "/workspaces/.codespaces/shared/first-run-notice.txt" + fi + mkdir -p "$HOME/.config/vscode-dev-containers" + # Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it + ((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &) +fi + +# Set the default git editor if not already set +if [ -z "$(git config --get core.editor)" ] && [ -z "${GIT_EDITOR}" ]; then + if [ "${TERM_PROGRAM}" = "vscode" ]; then + if [[ -n $(command -v code-insiders) && -z $(command -v code) ]]; then + export GIT_EDITOR="code-insiders --wait" + else + export GIT_EDITOR="code --wait" + fi + fi +fi + +EOF +)" + +# code shim, it fallbacks to code-insiders if code is not available +cat << 'EOF' > /usr/local/bin/code +#!/bin/sh + +get_in_path_except_current() { + which -a "$1" | grep -A1 "$0" | grep -v "$0" +} + +code="$(get_in_path_except_current code)" + +if [ -n "$code" ]; then + exec "$code" "$@" +elif [ "$(command -v code-insiders)" ]; then + exec code-insiders "$@" +else + echo "code or code-insiders is not installed" >&2 + exit 127 +fi +EOF +chmod +x /usr/local/bin/code + +# systemctl shim - tells people to use 'service' if systemd is not running +cat << 'EOF' > /usr/local/bin/systemctl +#!/bin/sh +set -e +if [ -d "/run/systemd/system" ]; then + exec /bin/systemctl/systemctl "$@" +else + echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services instead. e.g.: \n\nservice --status-all' +fi +EOF +chmod +x /usr/local/bin/systemctl + +# Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme +codespaces_bash="$(cat \ +<<'EOF' + +# Codespaces bash prompt theme +__bash_prompt() { + local userpart='`export XIT=$? \ + && [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \ + && [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`' + local gitbranch='`\ + if [ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ]; then \ + export BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null); \ + if [ "${BRANCH}" != "" ]; then \ + echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \ + && if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \ + echo -n " \[\033[1;33m\]✗"; \ + fi \ + && echo -n "\[\033[0;36m\]) "; \ + fi; \ + fi`' + local lightblue='\[\033[1;34m\]' + local removecolor='\[\033[0m\]' + PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ " + unset -f __bash_prompt +} +__bash_prompt + +EOF +)" + +codespaces_zsh="$(cat \ +<<'EOF' +# Codespaces zsh prompt theme +__zsh_prompt() { + local prompt_username + if [ ! -z "${GITHUB_USER}" ]; then + prompt_username="@${GITHUB_USER}" + else + prompt_username="%n" + fi + PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}➜ :%{$fg_bold[red]%}➜ )" # User/exit code arrow + PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd + PROMPT+='$([ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ] && git_prompt_info)' # Git status + PROMPT+='%{$fg[white]%}$ %{$reset_color%}' + unset -f __zsh_prompt +} +ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}" +ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} " +ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})" +ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})" +__zsh_prompt + +EOF +)" + +# Add RC snippet and custom bash prompt +if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then + echo "${rc_snippet}" >> /etc/bash.bashrc + echo "${codespaces_bash}" >> "${user_rc_path}/.bashrc" + echo 'export PROMPT_DIRTRIM=4' >> "${user_rc_path}/.bashrc" + if [ "${USERNAME}" != "root" ]; then + echo "${codespaces_bash}" >> "/root/.bashrc" + echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc" + fi + chown ${USERNAME}:${group_name} "${user_rc_path}/.bashrc" + RC_SNIPPET_ALREADY_ADDED="true" +fi + +# Optionally install and configure zsh and Oh My Zsh! +if [ "${INSTALL_ZSH}" = "true" ]; then + if ! type zsh > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get install -y zsh + fi + if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then + echo "${rc_snippet}" >> /etc/zsh/zshrc + ZSH_ALREADY_INSTALLED="true" + fi + + # Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme. + # See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script. + oh_my_install_dir="${user_rc_path}/.oh-my-zsh" + if [ ! -d "${oh_my_install_dir}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then + template_path="${oh_my_install_dir}/templates/zshrc.zsh-template" + user_rc_file="${user_rc_path}/.zshrc" + umask g-w,o-w + mkdir -p ${oh_my_install_dir} + git clone --depth=1 \ + -c core.eol=lf \ + -c core.autocrlf=false \ + -c fsck.zeroPaddedFilemode=ignore \ + -c fetch.fsck.zeroPaddedFilemode=ignore \ + -c receive.fsck.zeroPaddedFilemode=ignore \ + "https://github.com/ohmyzsh/ohmyzsh" "${oh_my_install_dir}" 2>&1 + echo -e "$(cat "${template_path}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${user_rc_file} + sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${user_rc_file} + + mkdir -p ${oh_my_install_dir}/custom/themes + echo "${codespaces_zsh}" > "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme" + # Shrink git while still enabling updates + cd "${oh_my_install_dir}" + git repack -a -d -f --depth=1 --window=1 + # Copy to non-root user if one is specified + if [ "${USERNAME}" != "root" ]; then + cp -rf "${user_rc_file}" "${oh_my_install_dir}" /root + chown -R ${USERNAME}:${group_name} "${user_rc_path}" + fi + fi +fi + +# Persist image metadata info, script if meta.env found in same directory +meta_info_script="$(cat << 'EOF' +#!/bin/sh +. /usr/local/etc/vscode-dev-containers/meta.env + +# Minimal output +if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then + echo "${VERSION}" + exit 0 +elif [ "$1" = "release" ]; then + echo "${GIT_REPOSITORY_RELEASE}" + exit 0 +elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then + echo "${CONTENTS_URL}" + exit 0 +fi + +#Full output +echo +echo "Development container image information" +echo +if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi +if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi +if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi +if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi +if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi +if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi +if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi +echo +EOF +)" +if [ -f "${SCRIPT_DIR}/meta.env" ]; then + mkdir -p /usr/local/etc/vscode-dev-containers/ + cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env + echo "${meta_info_script}" > /usr/local/bin/devcontainer-info + chmod +x /usr/local/bin/devcontainer-info +fi + +# Write marker file +mkdir -p "$(dirname "${MARKER_FILE}")" +echo -e "\ + PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ + LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ + EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ + RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\ + ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}" + +echo "Done!" diff --git a/collection/desktop-lite/feature.json b/collection/desktop-lite/feature.json new file mode 100644 index 0000000..4ad8b8d --- /dev/null +++ b/collection/desktop-lite/feature.json @@ -0,0 +1,39 @@ +{ + "id": "desktop-lite", + "name": "Light-weight desktop (Fluxbox)", + "options": { + "version": { + "type": "string", + "enum": ["latest"], + "default": "latest", + "description": "Currently unused." + }, + "password": { + "type": "string", + "proposals": ["vscode","codespaces","password"], + "default": "vscode", + "description": "Enter a password for desktop connections" + }, + "webPort": { + "type": "string", + "proposals": ["6080"], + "default": "6080", + "description": "Enter a port for the desktop web client" + }, + "vncPort": { + "type": "string", + "proposals": ["5901"], + "default": "5901", + "description": "Enter a port for the desktop VNC server" + } + }, + "init": true, + "entrypoint": "/usr/local/share/desktop-init.sh", + "containerEnv": { + "DISPLAY": ":1" + }, + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/desktop-lite/install.sh b/collection/desktop-lite/install.sh new file mode 100644 index 0000000..aeddac3 --- /dev/null +++ b/collection/desktop-lite/install.sh @@ -0,0 +1,394 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/desktop-lite.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./desktop-lite-debian.sh [non-root user] [Desktop password] [Install web client flag] [VNC port] [Web Port] + +USERNAME=${1:-"automatic"} +VNC_PASSWORD=${2:-"vscode"} +INSTALL_NOVNC=${3:-"true"} +VNC_PORT="${4:-5901}" +NOVNC_PORT="${5:-6080}" + +NOVNC_VERSION=1.2.0 +WEBSOCKETIFY_VERSION=0.10.0 + +package_list=" + tigervnc-standalone-server \ + tigervnc-common \ + fluxbox \ + dbus-x11 \ + x11-utils \ + x11-xserver-utils \ + xdg-utils \ + fbautostart \ + at-spi2-core \ + xterm \ + eterm \ + nautilus\ + mousepad \ + seahorse \ + gnome-icon-theme \ + gnome-keyring \ + libx11-dev \ + libxkbfile-dev \ + libsecret-1-dev \ + libgbm-dev \ + libnotify4 \ + libnss3 \ + libxss1 \ + libasound2 \ + xfonts-base \ + xfonts-terminus \ + fonts-noto \ + fonts-wqy-microhei \ + fonts-droid-fallback \ + htop \ + ncdu \ + curl \ + ca-certificates\ + unzip \ + nano \ + locales" + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi +# Add default Fluxbox config files if none are already present +fluxbox_apps="$(cat \ +<< 'EOF' +[transient] (role=GtkFileChooserDialog) + [Dimensions] {70% 70%} + [Position] (CENTER) {0 0} +[end] +EOF +)" + +fluxbox_init="$(cat \ +<< 'EOF' +session.configVersion: 13 +session.menuFile: ~/.fluxbox/menu +session.keyFile: ~/.fluxbox/keys +session.styleFile: /usr/share/fluxbox/styles/qnx-photon +session.screen0.workspaces: 1 +session.screen0.workspacewarping: false +session.screen0.toolbar.widthPercent: 100 +session.screen0.strftimeFormat: %a %l:%M %p +session.screen0.toolbar.tools: RootMenu, clock, iconbar, systemtray +session.screen0.workspaceNames: One, +EOF +)" + +fluxbox_menu="$(cat \ +<< 'EOF' +[begin] ( Application Menu ) + [exec] (File Manager) { nautilus ~ } <> + [exec] (Text Editor) { mousepad } <> + [exec] (Terminal) { tilix -w ~ -e $(readlink -f /proc/$$/exe) -il } <> + [exec] (Web Browser) { x-www-browser --disable-dev-shm-usage } <> + [submenu] (System) {} + [exec] (Set Resolution) { tilix -t "Set Resolution" -e bash /usr/local/bin/set-resolution } <> + [exec] (Edit Application Menu) { mousepad ~/.fluxbox/menu } <> + [exec] (Passwords and Keys) { seahorse } <> + [exec] (Top Processes) { tilix -t "Top" -e htop } <> + [exec] (Disk Utilization) { tilix -t "Disk Utilization" -e ncdu / } <> + [exec] (Editres) {editres} <> + [exec] (Xfontsel) {xfontsel} <> + [exec] (Xkill) {xkill} <> + [exec] (Xrefresh) {xrefresh} <> + [end] + [config] (Configuration) + [workspaces] (Workspaces) +[end] +EOF +)" + +# Copy config files if the don't already exist +copy_fluxbox_config() { + local target_dir="$1" + mkdir -p "${target_dir}/.fluxbox" + touch "${target_dir}/.Xmodmap" + if [ ! -e "${target_dir}/.fluxbox/apps" ]; then + echo "${fluxbox_apps}" > "${target_dir}/.fluxbox/apps" + fi + if [ ! -e "${target_dir}/.fluxbox/init" ]; then + echo "${fluxbox_init}" > "${target_dir}/.fluxbox/init" + fi + if [ ! -e "${target_dir}/.fluxbox/menu" ]; then + echo "${fluxbox_menu}" > "${target_dir}/.fluxbox/menu" + fi +} + + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +apt_get_update_if_needed + +# On older Ubuntu, Tilix is in a PPA. on Debian strech its in backports. +if [[ -z $(apt-cache --names-only search ^tilix$) ]]; then + . /etc/os-release + if [ "${ID}" = "ubuntu" ]; then + apt-get install -y --no-install-recommends apt-transport-https software-properties-common + add-apt-repository -y ppa:webupd8team/terminix + elif [ "${VERSION_CODENAME}" = "stretch" ]; then + echo "deb http://deb.debian.org/debian stretch-backports main" > /etc/apt/sources.list.d/stretch-backports.list + fi + apt-get update + if [[ -z $(apt-cache --names-only search ^tilix$) ]]; then + echo "(!) WARNING: Tilix not available on ${ID} ${VERSION_CODENAME} architecture $(uname -m). Skipping." + else + package_list="${package_list} tilix" + fi +else + package_list="${package_list} tilix" +fi + +# Install X11, fluxbox and VS Code dependencies +check_packages ${package_list} + +# Install Emoji font if available in distro - Available in Debian 10+, Ubuntu 18.04+ +if dpkg-query -W fonts-noto-color-emoji > /dev/null 2>&1 && ! dpkg -s fonts-noto-color-emoji > /dev/null 2>&1; then + apt-get -y install --no-install-recommends fonts-noto-color-emoji +fi + +# Check at least one locale exists +if ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then + echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen + locale-gen +fi + +# Install the Cascadia Code fonts - https://github.com/microsoft/cascadia-code +if [ ! -d "/usr/share/fonts/truetype/cascadia" ]; then + curl -sSL https://github.com/microsoft/cascadia-code/releases/download/v2008.25/CascadiaCode-2008.25.zip -o /tmp/cascadia-fonts.zip + unzip /tmp/cascadia-fonts.zip -d /tmp/cascadia-fonts + mkdir -p /usr/share/fonts/truetype/cascadia + mv /tmp/cascadia-fonts/ttf/* /usr/share/fonts/truetype/cascadia/ + rm -rf /tmp/cascadia-fonts.zip /tmp/cascadia-fonts +fi + +# Install noVNC +if [ "${INSTALL_NOVNC}" = "true" ] && [ ! -d "/usr/local/novnc" ]; then + mkdir -p /usr/local/novnc + curl -sSL https://github.com/novnc/noVNC/archive/v${NOVNC_VERSION}.zip -o /tmp/novnc-install.zip + unzip /tmp/novnc-install.zip -d /usr/local/novnc + cp /usr/local/novnc/noVNC-${NOVNC_VERSION}/vnc.html /usr/local/novnc/noVNC-${NOVNC_VERSION}/index.html + curl -sSL https://github.com/novnc/websockify/archive/v${WEBSOCKETIFY_VERSION}.zip -o /tmp/websockify-install.zip + unzip /tmp/websockify-install.zip -d /usr/local/novnc + ln -s /usr/local/novnc/websockify-${WEBSOCKETIFY_VERSION} /usr/local/novnc/noVNC-${NOVNC_VERSION}/utils/websockify + rm -f /tmp/websockify-install.zip /tmp/novnc-install.zip + + # Install noVNC dependencies and use them. + if ! dpkg -s python3-minimal python3-numpy > /dev/null 2>&1; then + apt-get -y install --no-install-recommends python3-minimal python3-numpy + fi + sed -i -E 's/^python /python3 /' /usr/local/novnc/websockify-${WEBSOCKETIFY_VERSION}/run +fi + +# Set up folders for scripts and init files +mkdir -p /var/run/dbus /usr/local/etc/vscode-dev-containers/ + +# Script to change resolution of desktop +cat << EOF > /usr/local/bin/set-resolution +#!/bin/bash +RESOLUTION=\${1:-\${VNC_RESOLUTION:-1920x1080}} +DPI=\${2:-\${VNC_DPI:-96}} +IGNORE_ERROR=\${3:-"false"} +if [ -z "\$1" ]; then + echo -e "**Current Settings **\n" + xrandr + echo -n -e "\nEnter new resolution (WIDTHxHEIGHT, blank for \${RESOLUTION}, Ctrl+C to abort).\n> " + read NEW_RES + if [ "\${NEW_RES}" != "" ]; then + RESOLUTION=\${NEW_RES} + fi + if ! echo "\${RESOLUTION}" | grep -E '[0-9]+x[0-9]+' > /dev/null; then + echo -e "\nInvalid resolution format!\n" + exit 1 + fi + if [ -z "\$2" ]; then + echo -n -e "\nEnter new DPI (blank for \${DPI}, Ctrl+C to abort).\n> " + read NEW_DPI + if [ "\${NEW_DPI}" != "" ]; then + DPI=\${NEW_DPI} + fi + fi +fi + +xrandr --fb \${RESOLUTION} --dpi \${DPI} > /dev/null 2>&1 + +if [ \$? -ne 0 ] && [ "\${IGNORE_ERROR}" != "true" ]; then + echo -e "\nFAILED TO SET RESOLUTION!\n" + exit 1 +fi + +echo -e "\nSuccess!\n" +EOF + +# Container ENTRYPOINT script +cat << EOF > /usr/local/share/desktop-init.sh +#!/bin/bash + +user_name="${USERNAME}" +group_name="$(id -gn ${USERNAME})" +LOG=/tmp/container-init.log + +export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-"autolaunch:"}" +export DISPLAY="${DISPLAY:-:1}" +export VNC_RESOLUTION="${VNC_RESOLUTION:-1440x768x16}" +export LANG="${LANG:-"en_US.UTF-8"}" +export LANGUAGE="${LANGUAGE:-"en_US.UTF-8"}" + +# Execute the command it not already running +startInBackgroundIfNotRunning() +{ + log "Starting \$1." + echo -e "\n** \$(date) **" | sudoIf tee -a /tmp/\$1.log > /dev/null + if ! pidof \$1 > /dev/null; then + keepRunningInBackground "\$@" + while ! pidof \$1 > /dev/null; do + sleep 1 + done + log "\$1 started." + else + echo "\$1 is already running." | sudoIf tee -a /tmp/\$1.log > /dev/null + log "\$1 is already running." + fi +} + +# Keep command running in background +keepRunningInBackground() +{ + (\$2 bash -c "while :; do echo [\\\$(date)] Process started.; \$3; echo [\\\$(date)] Process exited!; sleep 5; done 2>&1" | sudoIf tee -a /tmp/\$1.log > /dev/null & echo "\$!" | sudoIf tee /tmp/\$1.pid > /dev/null) +} + +# Use sudo to run as root when required +sudoIf() +{ + if [ "\$(id -u)" -ne 0 ]; then + sudo "\$@" + else + "\$@" + fi +} + +# Use sudo to run as non-root user if not already running +sudoUserIf() +{ + if [ "\$(id -u)" -eq 0 ] && [ "\${user_name}" != "root" ]; then + sudo -u \${user_name} "\$@" + else + "\$@" + fi +} + +# Log messages +log() +{ + echo -e "[\$(date)] \$@" | sudoIf tee -a \$LOG > /dev/null +} + +log "** SCRIPT START **" + +# Start dbus. +log 'Running "/etc/init.d/dbus start".' +if [ -f "/var/run/dbus/pid" ] && ! pidof dbus-daemon > /dev/null; then + sudoIf rm -f /var/run/dbus/pid +fi +sudoIf /etc/init.d/dbus start 2>&1 | sudoIf tee -a /tmp/dbus-daemon-system.log > /dev/null +while ! pidof dbus-daemon > /dev/null; do + sleep 1 +done + +# Startup tigervnc server and fluxbox +sudo rm -rf /tmp/.X11-unix /tmp/.X*-lock +mkdir -p /tmp/.X11-unix +sudoIf chmod 1777 /tmp/.X11-unix +sudoIf chown root:\${group_name} /tmp/.X11-unix +if [ "\$(echo "\${VNC_RESOLUTION}" | tr -cd 'x' | wc -c)" = "1" ]; then VNC_RESOLUTION=\${VNC_RESOLUTION}x16; fi +screen_geometry="\${VNC_RESOLUTION%*x*}" +screen_depth="\${VNC_RESOLUTION##*x}" +startInBackgroundIfNotRunning "Xtigervnc" sudoUserIf "tigervncserver \${DISPLAY} -geometry \${screen_geometry} -depth \${screen_depth} -rfbport ${VNC_PORT} -dpi \${VNC_DPI:-96} -localhost -desktop fluxbox -fg -passwd /usr/local/etc/vscode-dev-containers/vnc-passwd" + +# Spin up noVNC if installed and not runnning. +if [ -d "/usr/local/novnc" ] && [ "\$(ps -ef | grep /usr/local/novnc/noVNC*/utils/launch.sh | grep -v grep)" = "" ]; then + keepRunningInBackground "noVNC" sudoIf "/usr/local/novnc/noVNC*/utils/launch.sh --listen ${NOVNC_PORT} --vnc localhost:${VNC_PORT}" + log "noVNC started." +else + log "noVNC is already running or not installed." +fi + +# Run whatever was passed in +log "Executing \"\$@\"." +exec "\$@" +log "** SCRIPT EXIT **" +EOF + +echo "${VNC_PASSWORD}" | vncpasswd -f > /usr/local/etc/vscode-dev-containers/vnc-passwd +chmod +x /usr/local/share/desktop-init.sh /usr/local/bin/set-resolution + +# Set up fluxbox config +copy_fluxbox_config "/root" +if [ "${USERNAME}" != "root" ]; then + copy_fluxbox_config "/home/${USERNAME}" + chown -R ${USERNAME} /home/${USERNAME}/.Xmodmap /home/${USERNAME}/.fluxbox +fi + +cat << EOF + + +You now have a working desktop! Connect to in one of the following ways: + +- Forward port ${NOVNC_PORT} and use a web browser start the noVNC client (recommended) +- Forward port ${VNC_PORT} using VS Code client and connect using a VNC Viewer + +In both cases, use the password "${VNC_PASSWORD}" when connecting + +(*) Done! + +EOF + diff --git a/collection/docker-from-docker/feature.json b/collection/docker-from-docker/feature.json new file mode 100644 index 0000000..c7d684e --- /dev/null +++ b/collection/docker-from-docker/feature.json @@ -0,0 +1,41 @@ +{ + "id": "docker-from-docker", + "name": "Docker (Moby) support, reuse host Docker Engine (Docker-from-Docker)", + "options": { + "version": { + "type": "string", + "proposals": ["latest", "20.10"], + "default": "latest", + "description": "Select or enter a Docker/Moby CLI version. (Availability can vary by OS version.)" + }, + "moby": { + "type": "boolean", + "default": true, + "description": "Install OSS Moby build instead of Docker CE" + }, + "dockerDashComposeVersion": { + "type": "string", + "enum": ["v1", "v2" ], + "default": "v1", + "description": "Compose version to use for docker-compose (v1 or v2)" + } + }, + "entrypoint": "/usr/local/share/docker-init.sh", + "containerEnv": { + "DOCKER_BUILDKIT": "1" + }, + "extensions": [ + "ms-azuretools.vscode-docker" + ], + "mounts": [ + { + "source":"/var/run/docker.sock", + "target":"/var/run/docker-host.sock", + "type":"bind" + } + ], + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/docker-from-docker/install.sh b/collection/docker-from-docker/install.sh new file mode 100644 index 0000000..1102ae9 --- /dev/null +++ b/collection/docker-from-docker/install.sh @@ -0,0 +1,354 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./docker-debian.sh [enable non-root docker socket access flag] [source socket] [target socket] [non-root user] [use moby] [CLI version] [Major version for docker-compose] + +ENABLE_NONROOT_DOCKER=${1:-"true"} +SOURCE_SOCKET=${2:-"/var/run/docker-host.sock"} +TARGET_SOCKET=${3:-"/var/run/docker.sock"} +USERNAME=${4:-"automatic"} +USE_MOBY=${5:-"true"} +DOCKER_VERSION=${6:-"latest"} +DOCKER_DASH_COMPOSE_VERSION=${7:-"v1"} # v1 or v2 +MICROSOFT_GPG_KEYS_URI="https://packages.microsoft.com/keys/microsoft.asc" +DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES="buster bullseye bionic focal jammy" +DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES="buster bullseye bionic focal hirsute impish jammy" + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Figure out correct version of a three part version number is not passed +find_version_from_git_tags() { + local variable_name=$1 + local requested_version=${!variable_name} + if [ "${requested_version}" = "none" ]; then return; fi + local repository=$2 + local prefix=${3:-"tags/v"} + local separator=${4:-"."} + local last_part_optional=${5:-"false"} + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local escaped_separator=${separator//./\\.} + local last_part + if [ "${last_part_optional}" = "true" ]; then + last_part="(${escaped_separator}[0-9]+)?" + else + last_part="${escaped_separator}[0-9]+" + fi + local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" + local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" + else + set +e + declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + fi + if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then + echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 + exit 1 + fi + echo "${variable_name}=${!variable_name}" +} + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Install dependencies +check_packages apt-transport-https curl ca-certificates gnupg2 dirmngr +if ! type git > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install git +fi + +# Source /etc/os-release to get OS info +. /etc/os-release +# Fetch host/container arch. +architecture="$(dpkg --print-architecture)" + +# Check if distro is suppported +if [ "${USE_MOBY}" = "true" ]; then + # 'get_common_setting' allows attribute to be updated remotely + get_common_setting DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES + if [[ "${DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES}" != *"${VERSION_CODENAME}"* ]]; then + err "Unsupported distribution version '${VERSION_CODENAME}'. To resolve, either: (1) set feature option '\"moby\": false' , or (2) choose a compatible OS distribution" + err "Support distributions include: ${DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES}" + exit 1 + fi + echo "Distro codename '${VERSION_CODENAME}' matched filter '${DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES}'" +else + get_common_setting DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES + if [[ "${DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES}" != *"${VERSION_CODENAME}"* ]]; then + err "Unsupported distribution version '${VERSION_CODENAME}'. To resolve, please choose a compatible OS distribution" + err "Support distributions include: ${DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES}" + exit 1 + fi + echo "Distro codename '${VERSION_CODENAME}' matched filter '${DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES}'" +fi + +# Set up the necessary apt repos (either Microsoft's or Docker's) +if [ "${USE_MOBY}" = "true" ]; then + + cli_package_name="moby-cli" + + # Import key safely and import Microsoft apt repo + get_common_setting MICROSOFT_GPG_KEYS_URI + curl -sSL ${MICROSOFT_GPG_KEYS_URI} | gpg --dearmor > /usr/share/keyrings/microsoft-archive-keyring.gpg + echo "deb [arch=${architecture} signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/repos/microsoft-${ID}-${VERSION_CODENAME}-prod ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/microsoft.list +else + # Name of proprietary engine package + cli_package_name="docker-ce-cli" + + # Import key safely and import Docker apt repo + curl -fsSL https://download.docker.com/linux/${ID}/gpg | gpg --dearmor > /usr/share/keyrings/docker-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list +fi + +# Refresh apt lists +apt-get update + +# Soft version matching for CLI +if [ "${DOCKER_VERSION}" = "latest" ] || [ "${DOCKER_VERSION}" = "lts" ] || [ "${DOCKER_VERSION}" = "stable" ]; then + # Empty, meaning grab whatever "latest" is in apt repo + cli_version_suffix="" +else + # Fetch a valid version from the apt-cache (eg: the Microsoft repo appends +azure, breakfix, etc...) + docker_version_dot_escaped="${DOCKER_VERSION//./\\.}" + docker_version_dot_plus_escaped="${docker_version_dot_escaped//+/\\+}" + # Regex needs to handle debian package version number format: https://www.systutorials.com/docs/linux/man/5-deb-version/ + docker_version_regex="^(.+:)?${docker_version_dot_plus_escaped}([\\.\\+ ~:-]|$)" + set +e # Don't exit if finding version fails - will handle gracefully + cli_version_suffix="=$(apt-cache madison ${cli_package_name} | awk -F"|" '{print $2}' | sed -e 's/^[ \t]*//' | grep -E -m 1 "${docker_version_regex}")" + set -e + if [ -z "${cli_version_suffix}" ] || [ "${cli_version_suffix}" = "=" ]; then + echo "(!) No full or partial Docker / Moby version match found for \"${DOCKER_VERSION}\" on OS ${ID} ${VERSION_CODENAME} (${architecture}). Available versions:" + apt-cache madison ${cli_package_name} | awk -F"|" '{print $2}' | grep -oP '^(.+:)?\K.+' + exit 1 + fi + echo "cli_version_suffix ${cli_version_suffix}" +fi + +# Install Docker / Moby CLI if not already installed +if type docker > /dev/null 2>&1; then + echo "Docker / Moby CLI already installed." +else + if [ "${USE_MOBY}" = "true" ]; then + apt-get -y install --no-install-recommends moby-cli${cli_version_suffix} moby-buildx + apt-get -y install --no-install-recommends moby-compose || echo "(*) Package moby-compose (Docker Compose v2) not available for OS ${ID} ${VERSION_CODENAME} (${architecture}). Skipping." + else + apt-get -y install --no-install-recommends docker-ce-cli${cli_version_suffix} + fi +fi + +# Install Docker Compose if not already installed and is on a supported architecture +if type docker-compose > /dev/null 2>&1; then + echo "Docker Compose already installed." +else + TARGET_COMPOSE_ARCH="$(uname -m)" + if [ "${TARGET_COMPOSE_ARCH}" = "amd64" ]; then + TARGET_COMPOSE_ARCH="x86_64" + fi + if [ "${TARGET_COMPOSE_ARCH}" != "x86_64" ]; then + # Use pip to get a version that runns on this architecture + if ! dpkg -s python3-minimal python3-pip libffi-dev python3-venv > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install python3-minimal python3-pip libffi-dev python3-venv + fi + export PIPX_HOME=/usr/local/pipx + mkdir -p ${PIPX_HOME} + export PIPX_BIN_DIR=/usr/local/bin + export PYTHONUSERBASE=/tmp/pip-tmp + export PIP_CACHE_DIR=/tmp/pip-tmp/cache + pipx_bin=pipx + if ! type pipx > /dev/null 2>&1; then + pip3 install --disable-pip-version-check --no-cache-dir --user pipx + pipx_bin=/tmp/pip-tmp/bin/pipx + fi + ${pipx_bin} install --pip-args '--no-cache-dir --force-reinstall' docker-compose + rm -rf /tmp/pip-tmp + else + compose_v1_version="1" + find_version_from_git_tags compose_v1_version "https://github.com/docker/compose" "tags/" + echo "(*) Installing docker-compose ${compose_v1_version}..." + curl -fsSL "https://github.com/docker/compose/releases/download/${compose_v1_version}/docker-compose-Linux-x86_64" -o /usr/local/bin/docker-compose + chmod +x /usr/local/bin/docker-compose + fi +fi + +# Install docker-compose switch if not already installed - https://github.com/docker/compose-switch#manual-installation +current_v1_compose_path="$(which docker-compose)" +target_v1_compose_path="$(dirname "${current_v1_compose_path}")/docker-compose-v1" +if ! type compose-switch > /dev/null 2>&1; then + echo "(*) Installing compose-switch..." + compose_switch_version="latest" + find_version_from_git_tags compose_switch_version "https://github.com/docker/compose-switch" + curl -fsSL "https://github.com/docker/compose-switch/releases/download/v${compose_switch_version}/docker-compose-linux-${architecture}" -o /usr/local/bin/compose-switch + chmod +x /usr/local/bin/compose-switch + # TODO: Verify checksum once available: https://github.com/docker/compose-switch/issues/11 + + # Setup v1 CLI as alternative in addition to compose-switch (which maps to v2) + mv "${current_v1_compose_path}" "${target_v1_compose_path}" + update-alternatives --install /usr/local/bin/docker-compose docker-compose /usr/local/bin/compose-switch 99 + update-alternatives --install /usr/local/bin/docker-compose docker-compose "${target_v1_compose_path}" 1 +fi +if [ "${DOCKER_DASH_COMPOSE_VERSION}" = "v1" ]; then + update-alternatives --set docker-compose "${target_v1_compose_path}" +else + update-alternatives --set docker-compose /usr/local/bin/compose-switch +fi + +# If init file already exists, exit +if [ -f "/usr/local/share/docker-init.sh" ]; then + exit 0 +fi +echo "docker-init doesnt exist, adding..." + +# By default, make the source and target sockets the same +if [ "${SOURCE_SOCKET}" != "${TARGET_SOCKET}" ]; then + touch "${SOURCE_SOCKET}" + ln -s "${SOURCE_SOCKET}" "${TARGET_SOCKET}" +fi + +# Add a stub if not adding non-root user access, user is root +if [ "${ENABLE_NONROOT_DOCKER}" = "false" ] || [ "${USERNAME}" = "root" ]; then + echo -e '#!/usr/bin/env bash\nexec "$@"' > /usr/local/share/docker-init.sh + chmod +x /usr/local/share/docker-init.sh + exit 0 +fi + +# Setup a docker group in the event the docker socket's group is not root +if ! grep -qE '^docker:' /etc/group; then + groupadd --system docker +fi +usermod -aG docker "${USERNAME}" +DOCKER_GID="$(grep -oP '^docker:x:\K[^:]+' /etc/group)" + +# If enabling non-root access and specified user is found, setup socat and add script +chown -h "${USERNAME}":root "${TARGET_SOCKET}" +if ! dpkg -s socat > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install socat +fi +tee /usr/local/share/docker-init.sh > /dev/null \ +<< EOF +#!/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 -e + +SOCAT_PATH_BASE=/tmp/vscr-docker-from-docker +SOCAT_LOG=\${SOCAT_PATH_BASE}.log +SOCAT_PID=\${SOCAT_PATH_BASE}.pid + +# Wrapper function to only use sudo if not already root +sudoIf() +{ + if [ "\$(id -u)" -ne 0 ]; then + sudo "\$@" + else + "\$@" + fi +} + +# Log messages +log() +{ + echo -e "[\$(date)] \$@" | sudoIf tee -a \${SOCAT_LOG} > /dev/null +} + +echo -e "\n** \$(date) **" | sudoIf tee -a \${SOCAT_LOG} > /dev/null +log "Ensuring ${USERNAME} has access to ${SOURCE_SOCKET} via ${TARGET_SOCKET}" + +# If enabled, try to update the docker group with the right GID. If the group is root, +# fall back on using socat to forward the docker socket to another unix socket so +# that we can set permissions on it without affecting the host. +if [ "${ENABLE_NONROOT_DOCKER}" = "true" ] && [ "${SOURCE_SOCKET}" != "${TARGET_SOCKET}" ] && [ "${USERNAME}" != "root" ] && [ "${USERNAME}" != "0" ]; then + SOCKET_GID=\$(stat -c '%g' ${SOURCE_SOCKET}) + if [ "\${SOCKET_GID}" != "0" ] && [ "\${SOCKET_GID}" != "${DOCKER_GID}" ] && ! grep -E ".+:x:\${SOCKET_GID}" /etc/group; then + sudoIf groupmod --gid "\${SOCKET_GID}" docker + else + # Enable proxy if not already running + if [ ! -f "\${SOCAT_PID}" ] || ! ps -p \$(cat \${SOCAT_PID}) > /dev/null; then + log "Enabling socket proxy." + log "Proxying ${SOURCE_SOCKET} to ${TARGET_SOCKET} for vscode" + sudoIf rm -rf ${TARGET_SOCKET} + (sudoIf socat UNIX-LISTEN:${TARGET_SOCKET},fork,mode=660,user=${USERNAME} UNIX-CONNECT:${SOURCE_SOCKET} 2>&1 | sudoIf tee -a \${SOCAT_LOG} > /dev/null & echo "\$!" | sudoIf tee \${SOCAT_PID} > /dev/null) + else + log "Socket proxy already running." + fi + fi + log "Success" +fi + +# Execute whatever commands were passed in (if any). This allows us +# to set this script to ENTRYPOINT while still executing the default CMD. +set +e +exec "\$@" +EOF +chmod +x /usr/local/share/docker-init.sh +chown ${USERNAME}:root /usr/local/share/docker-init.sh +echo "Done!" diff --git a/collection/docker-in-docker/feature.json b/collection/docker-in-docker/feature.json new file mode 100644 index 0000000..0eaaa80 --- /dev/null +++ b/collection/docker-in-docker/feature.json @@ -0,0 +1,42 @@ +{ + "id": "docker-in-docker", + "name": "Docker (Moby) support (Docker-in-Docker)", + "options": { + "version": { + "type": "string", + "proposals": ["latest", "20.10" ], + "default": "latest", + "description": "Select or enter a Docker/Moby Engine version. (Availability can vary by OS version.)" + }, + "moby": { + "type": "boolean", + "default": true, + "description": "Install OSS Moby build instead of Docker CE" + }, + "dockerDashComposeVersion": { + "type": "string", + "enum": ["v1", "v2" ], + "default": "v1", + "description": "Default version of Docker Compose (v1 or v2)" + } + }, + "entrypoint": "/usr/local/share/docker-init.sh", + "privileged": true, + "containerEnv": { + "DOCKER_BUILDKIT": "1" + }, + "extensions": [ + "ms-azuretools.vscode-docker" + ], + "mounts": [ + { + "source":"dind-var-lib-docker", + "target":"/var/lib/docker", + "type":"volume" + } + ], + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/docker-in-docker/install.sh b/collection/docker-in-docker/install.sh new file mode 100644 index 0000000..a525203 --- /dev/null +++ b/collection/docker-in-docker/install.sh @@ -0,0 +1,397 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker-in-docker.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./docker-in-docker-debian.sh [enable non-root docker access flag] [non-root user] [use moby] [Engine/CLI Version] [Major version for docker-compose] + +ENABLE_NONROOT_DOCKER=${1:-"true"} +USERNAME=${2:-"automatic"} +USE_MOBY=${3:-"true"} +DOCKER_VERSION=${4:-"latest"} # The Docker/Moby Engine + CLI should match in version +DOCKER_DASH_COMPOSE_VERSION=${5:-"v1"} # v1 or v2 +MICROSOFT_GPG_KEYS_URI="https://packages.microsoft.com/keys/microsoft.asc" +DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES="buster bullseye bionic focal jammy" +DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES="buster bullseye bionic focal hirsute impish jammy" + +# Default: Exit on any failure. +set -e + +# Setup STDERR. +err() { + echo "(!) $*" >&2 +} + +if [ "$(id -u)" -ne 0 ]; then + err 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +################### +# Helper Functions +# See: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/shared/utils.sh +################### + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Figure out correct version of a three part version number is not passed +find_version_from_git_tags() { + local variable_name=$1 + local requested_version=${!variable_name} + if [ "${requested_version}" = "none" ]; then return; fi + local repository=$2 + local prefix=${3:-"tags/v"} + local separator=${4:-"."} + local last_part_optional=${5:-"false"} + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local escaped_separator=${separator//./\\.} + local last_part + if [ "${last_part_optional}" = "true" ]; then + last_part="(${escaped_separator}[0-9]+)?" + else + last_part="${escaped_separator}[0-9]+" + fi + local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" + local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" + else + set +e + declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + fi + if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then + err "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 + exit 1 + fi + echo "${variable_name}=${!variable_name}" +} + +########################################### +# Start docker-in-docker installation +########################################### + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + + +# Source /etc/os-release to get OS info +. /etc/os-release +# Fetch host/container arch. +architecture="$(dpkg --print-architecture)" + +# Check if distro is suppported +if [ "${USE_MOBY}" = "true" ]; then + # 'get_common_setting' allows attribute to be updated remotely + get_common_setting DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES + if [[ "${DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES}" != *"${VERSION_CODENAME}"* ]]; then + err "Unsupported distribution version '${VERSION_CODENAME}'. To resolve, either: (1) set feature option '\"moby\": false' , or (2) choose a compatible OS distribution" + err "Support distributions include: ${DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES}" + exit 1 + fi + echo "Distro codename '${VERSION_CODENAME}' matched filter '${DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES}'" +else + get_common_setting DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES + if [[ "${DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES}" != *"${VERSION_CODENAME}"* ]]; then + err "Unsupported distribution version '${VERSION_CODENAME}'. To resolve, please choose a compatible OS distribution" + err "Support distributions include: ${DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES}" + exit 1 + fi + echo "Distro codename '${VERSION_CODENAME}' matched filter '${DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES}'" +fi + +# Install dependencies +check_packages apt-transport-https curl ca-certificates pigz iptables gnupg2 dirmngr +if ! type git > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install git +fi + +# Swap to legacy iptables for compatibility +if type iptables-legacy > /dev/null 2>&1; then + update-alternatives --set iptables /usr/sbin/iptables-legacy + update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy +fi + + + +# Set up the necessary apt repos (either Microsoft's or Docker's) +if [ "${USE_MOBY}" = "true" ]; then + + # Name of open source engine/cli + engine_package_name="moby-engine" + cli_package_name="moby-cli" + + # Import key safely and import Microsoft apt repo + get_common_setting MICROSOFT_GPG_KEYS_URI + curl -sSL ${MICROSOFT_GPG_KEYS_URI} | gpg --dearmor > /usr/share/keyrings/microsoft-archive-keyring.gpg + echo "deb [arch=${architecture} signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/repos/microsoft-${ID}-${VERSION_CODENAME}-prod ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/microsoft.list +else + # Name of licensed engine/cli + engine_package_name="docker-ce" + cli_package_name="docker-ce-cli" + + # Import key safely and import Docker apt repo + curl -fsSL https://download.docker.com/linux/${ID}/gpg | gpg --dearmor > /usr/share/keyrings/docker-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list +fi + +# Refresh apt lists +apt-get update + +# Soft version matching +if [ "${DOCKER_VERSION}" = "latest" ] || [ "${DOCKER_VERSION}" = "lts" ] || [ "${DOCKER_VERSION}" = "stable" ]; then + # Empty, meaning grab whatever "latest" is in apt repo + engine_version_suffix="" + cli_version_suffix="" +else + # Fetch a valid version from the apt-cache (eg: the Microsoft repo appends +azure, breakfix, etc...) + docker_version_dot_escaped="${DOCKER_VERSION//./\\.}" + docker_version_dot_plus_escaped="${docker_version_dot_escaped//+/\\+}" + # Regex needs to handle debian package version number format: https://www.systutorials.com/docs/linux/man/5-deb-version/ + docker_version_regex="^(.+:)?${docker_version_dot_plus_escaped}([\\.\\+ ~:-]|$)" + set +e # Don't exit if finding version fails - will handle gracefully + cli_version_suffix="=$(apt-cache madison ${cli_package_name} | awk -F"|" '{print $2}' | sed -e 's/^[ \t]*//' | grep -E -m 1 "${docker_version_regex}")" + engine_version_suffix="=$(apt-cache madison ${engine_package_name} | awk -F"|" '{print $2}' | sed -e 's/^[ \t]*//' | grep -E -m 1 "${docker_version_regex}")" + set -e + if [ -z "${engine_version_suffix}" ] || [ "${engine_version_suffix}" = "=" ] || [ -z "${cli_version_suffix}" ] || [ "${cli_version_suffix}" = "=" ] ; then + err "No full or partial Docker / Moby version match found for \"${DOCKER_VERSION}\" on OS ${ID} ${VERSION_CODENAME} (${architecture}). Available versions:" + apt-cache madison ${cli_package_name} | awk -F"|" '{print $2}' | grep -oP '^(.+:)?\K.+' + exit 1 + fi + echo "engine_version_suffix ${engine_version_suffix}" + echo "cli_version_suffix ${cli_version_suffix}" +fi + +# Install Docker / Moby CLI if not already installed +if type docker > /dev/null 2>&1 && type dockerd > /dev/null 2>&1; then + echo "Docker / Moby CLI and Engine already installed." +else + if [ "${USE_MOBY}" = "true" ]; then + # Install engine + set +e # Handle error gracefully + apt-get -y install --no-install-recommends moby-cli${cli_version_suffix} moby-buildx moby-engine${engine_version_suffix} + if [ $? -ne 0 ]; then + err "Packages for moby not available in OS ${ID} ${VERSION_CODENAME} (${architecture}). To resolve, either: (1) set feature option '\"moby\": false' , or (2) choose a compatible OS version (eg: 'ubuntu-20.04')." + exit 1 + fi + set -e + + # Install compose + apt-get -y install --no-install-recommends moby-compose || err "Package moby-compose (Docker Compose v2) not available for OS ${ID} ${VERSION_CODENAME} (${architecture}). Skipping." + else + apt-get -y install --no-install-recommends docker-ce-cli${cli_version_suffix} docker-ce${engine_version_suffix} + fi +fi + +echo "Finished installing docker / moby!" + +# Install Docker Compose if not already installed and is on a supported architecture +if type docker-compose > /dev/null 2>&1; then + echo "Docker Compose v1 already installed." +else + target_compose_arch="${architecture}" + if [ "${target_compose_arch}" = "amd64" ]; then + target_compose_arch="x86_64" + fi + if [ "${target_compose_arch}" != "x86_64" ]; then + # Use pip to get a version that runs on this architecture + if ! dpkg -s python3-minimal python3-pip libffi-dev python3-venv > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install python3-minimal python3-pip libffi-dev python3-venv + fi + export PIPX_HOME=/usr/local/pipx + mkdir -p ${PIPX_HOME} + export PIPX_BIN_DIR=/usr/local/bin + export PYTHONUSERBASE=/tmp/pip-tmp + export PIP_CACHE_DIR=/tmp/pip-tmp/cache + pipx_bin=pipx + if ! type pipx > /dev/null 2>&1; then + pip3 install --disable-pip-version-check --no-cache-dir --user pipx + pipx_bin=/tmp/pip-tmp/bin/pipx + fi + ${pipx_bin} install --pip-args '--no-cache-dir --force-reinstall' docker-compose + rm -rf /tmp/pip-tmp + else + compose_v1_version="1" + find_version_from_git_tags compose_v1_version "https://github.com/docker/compose" "tags/" + echo "(*) Installing docker-compose ${compose_v1_version}..." + curl -fsSL "https://github.com/docker/compose/releases/download/${compose_v1_version}/docker-compose-Linux-x86_64" -o /usr/local/bin/docker-compose + chmod +x /usr/local/bin/docker-compose + fi +fi + +# Install docker-compose switch if not already installed - https://github.com/docker/compose-switch#manual-installation +current_v1_compose_path="$(which docker-compose)" +target_v1_compose_path="$(dirname "${current_v1_compose_path}")/docker-compose-v1" +if ! type compose-switch > /dev/null 2>&1; then + echo "(*) Installing compose-switch..." + compose_switch_version="latest" + find_version_from_git_tags compose_switch_version "https://github.com/docker/compose-switch" + curl -fsSL "https://github.com/docker/compose-switch/releases/download/v${compose_switch_version}/docker-compose-linux-${architecture}" -o /usr/local/bin/compose-switch + chmod +x /usr/local/bin/compose-switch + # TODO: Verify checksum once available: https://github.com/docker/compose-switch/issues/11 + + # Setup v1 CLI as alternative in addition to compose-switch (which maps to v2) + mv "${current_v1_compose_path}" "${target_v1_compose_path}" + update-alternatives --install /usr/local/bin/docker-compose docker-compose /usr/local/bin/compose-switch 99 + update-alternatives --install /usr/local/bin/docker-compose docker-compose "${target_v1_compose_path}" 1 +fi +if [ "${DOCKER_DASH_COMPOSE_VERSION}" = "v1" ]; then + update-alternatives --set docker-compose "${target_v1_compose_path}" +else + update-alternatives --set docker-compose /usr/local/bin/compose-switch +fi + +# If init file already exists, exit +if [ -f "/usr/local/share/docker-init.sh" ]; then + echo "/usr/local/share/docker-init.sh already exists, so exiting." + exit 0 +fi +echo "docker-init doesnt exist, adding..." + +# Add user to the docker group +if [ "${ENABLE_NONROOT_DOCKER}" = "true" ]; then + if ! getent group docker > /dev/null 2>&1; then + groupadd docker + fi + + usermod -aG docker ${USERNAME} +fi + +tee /usr/local/share/docker-init.sh > /dev/null \ +<< 'EOF' +#!/bin/sh +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- + +set -e + +dockerd_start="$(cat << 'INNEREOF' + # explicitly remove dockerd and containerd PID file to ensure that it can start properly if it was stopped uncleanly + # ie: docker kill + find /run /var/run -iname 'docker*.pid' -delete || : + find /run /var/run -iname 'container*.pid' -delete || : + + ## Dind wrapper script from docker team, adapted to a function + # Maintained: https://github.com/moby/moby/blob/master/hack/dind + + export container=docker + + if [ -d /sys/kernel/security ] && ! mountpoint -q /sys/kernel/security; then + mount -t securityfs none /sys/kernel/security || { + echo >&2 'Could not mount /sys/kernel/security.' + echo >&2 'AppArmor detection and --privileged mode might break.' + } + fi + + # Mount /tmp (conditionally) + if ! mountpoint -q /tmp; then + mount -t tmpfs none /tmp + fi + + # cgroup v2: enable nesting + if [ -f /sys/fs/cgroup/cgroup.controllers ]; then + # move the processes from the root group to the /init group, + # otherwise writing subtree_control fails with EBUSY. + # An error during moving non-existent process (i.e., "cat") is ignored. + mkdir -p /sys/fs/cgroup/init + xargs -rn1 < /sys/fs/cgroup/cgroup.procs > /sys/fs/cgroup/init/cgroup.procs || : + # enable controllers + sed -e 's/ / +/g' -e 's/^/+/' < /sys/fs/cgroup/cgroup.controllers \ + > /sys/fs/cgroup/cgroup.subtree_control + fi + ## Dind wrapper over. + + # Handle DNS + set +e + cat /etc/resolv.conf | grep -i 'internal.cloudapp.net' + if [ $? -eq 0 ] + then + echo "Setting dockerd Azure DNS." + CUSTOMDNS="--dns 168.63.129.16" + else + echo "Not setting dockerd DNS manually." + CUSTOMDNS="" + fi + set -e + + # Start docker/moby engine + ( dockerd $CUSTOMDNS > /tmp/dockerd.log 2>&1 ) & +INNEREOF +)" + +# Start using sudo if not invoked as root +if [ "$(id -u)" -ne 0 ]; then + sudo /bin/sh -c "${dockerd_start}" +else + eval "${dockerd_start}" +fi + +set +e + +# Execute whatever commands were passed in (if any). This allows us +# to set this script to ENTRYPOINT while still executing the default CMD. +exec "$@" +EOF + +chmod +x /usr/local/share/docker-init.sh +chown ${USERNAME}:root /usr/local/share/docker-init.sh + +echo 'docker-in-docker-debian script has completed!' \ No newline at end of file diff --git a/collection/dotnet/feature.json b/collection/dotnet/feature.json new file mode 100644 index 0000000..b49e613 --- /dev/null +++ b/collection/dotnet/feature.json @@ -0,0 +1,28 @@ +{ + "id": "dotnet", + "name": "Dotnet CLI", + "options": { + "version": { + "type": "string", + "proposals": ["latest","6.0","5.0","3.1"], + "default": "latest", + "description": "Select or enter a dotnet CLI version. (Available versions may vary by Linux distribution.)" + }, + "runtimeOnly": { + "type":"boolean", + "default": false, + "description": "Install just the dotnet runtime if true, and sdk if false." + } + }, + "containerEnv": { + "DOTNET_ROOT": "/usr/local/dotnet", + "PATH": "${PATH}:${DOTNET_ROOT}" + }, + "extensions": [ + "ms-dotnettools.csharp" + ], + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/dotnet/install.sh b/collection/dotnet/install.sh new file mode 100644 index 0000000..f560186 --- /dev/null +++ b/collection/dotnet/install.sh @@ -0,0 +1,374 @@ +#!/bin/bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/dotnet.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./dotnet-debian.sh [.NET version] [.NET runtime only] [non-root user] [add TARGET_DOTNET_ROOT to rc files flag] [.NET root] [access group name] + +DOTNET_VERSION=${1:-"latest"} +DOTNET_RUNTIME_ONLY=${2:-"false"} +USERNAME=${3:-"automatic"} +UPDATE_RC=${4:-"true"} +TARGET_DOTNET_ROOT=${5:-"/usr/local/dotnet"} +ACCESS_GROUP=${6:-"dotnet"} + +MICROSOFT_GPG_KEYS_URI="https://packages.microsoft.com/keys/microsoft.asc" +DOTNET_ARCHIVE_ARCHITECTURES="amd64" +DOTNET_ARCHIVE_VERSION_CODENAMES="buster bullseye bionic focal hirsute" +# Feed URI sourced from the official dotnet-install.sh +# https://github.com/dotnet/install-scripts/blob/1b98b94a6f6d81cc4845eb88e0195fac67caa0a6/src/dotnet-install.sh#L1342-L1343 +DOTNET_CDN_FEED_URI="https://dotnetcli.azureedge.net" + +# Exit on failure. +set -e + +# Setup STDERR. +err() { + echo "(!) $*" >&2 +} + +# Ensure the appropriate root user is running the script. +if [ "$(id -u)" -ne 0 ]; then + err 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# Determine the appropriate non-root user. +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do + if id -u "${CURRENT_USER}" > /dev/null 2>&1; then + USERNAME="${CURRENT_USER}" + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +################### +# Helper Functions +################### + +# Cleanup temporary directory and associated files when exiting the script. +cleanup() { + EXIT_CODE=$? + set +e + if [[ -n "${TMP_DIR}" ]]; then + echo "Executing cleanup of tmp files" + rm -Rf "${TMP_DIR}" + fi + exit $EXIT_CODE +} +trap cleanup EXIT + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Add TARGET_DOTNET_ROOT variable into PATH in bashrc/zshrc files. +updaterc() { + if [ "${UPDATE_RC}" = "true" ]; then + echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." + if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/bash.bashrc + fi + if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/zsh/zshrc + fi + fi +} + +# Run apt-get if needed. +apt_get_update_if_needed() { + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Check if packages are installed and installs them if not. +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Get appropriate architecture name for .NET binaries for the target OS +get_architecture_name_for_target_os() { + local architecture + architecture="$(uname -m)" + case $architecture in + x86_64) architecture="x64";; + aarch64 | armv8*) architecture="arm64";; + *) err "Architecture ${architecture} unsupported"; exit 1 ;; + esac + + echo "${architecture}" +} + +# Soft version matching that resolves a version for a given package in the *current apt-cache* +# Return value is stored in first argument (the unprocessed version) +apt_cache_package_and_version_soft_match() { + # Version + local version_variable_name="$1" + local requested_version=${!version_variable_name} + # Package Name + local package_variable_name="$2" + local partial_package_name=${!package_variable_name} + local package_name + # Exit on no match? + local exit_on_no_match="${3:-true}" + local major_minor_version + + major_minor_version="$(echo "${requested_version}" | cut -d "." --field=1,2)" + package_name="$(apt-cache search "${partial_package_name}-[0-9].[0-9]" | awk -F" - " '{print $1}' | grep -m 1 "${partial_package_name}-${major_minor_version}")" + + # Ensure we've exported useful variables + . /etc/os-release + local architecture="$(dpkg --print-architecture)" + + dot_escaped="${requested_version//./\\.}" + dot_plus_escaped="${dot_escaped//+/\\+}" + # Regex needs to handle debian package version number format: https://www.systutorials.com/docs/linux/man/5-deb-version/ + version_regex="^(.+:)?${dot_plus_escaped}([\\.\\+ ~:-]|$)" + set +e # Don't exit if finding version fails - handle gracefully + fuzzy_version="$(apt-cache madison ${package_name} | awk -F"|" '{print $2}' | sed -e 's/^[ \t]*//' | grep -E -m 1 "${version_regex}")" + set -e + if [ -z "${fuzzy_version}" ]; then + echo "(!) No full or partial for package \"${package_name}\" match found in apt-cache for \"${requested_version}\" on OS ${ID} ${VERSION_CODENAME} (${architecture})." + + if $exit_on_no_match; then + echo "Available versions:" + apt-cache madison ${package_name} | awk -F"|" '{print $2}' | grep -oP '^(.+:)?\K.+' + exit 1 # Fail entire script + else + echo "Continuing to fallback method (if available)" + return 1; + fi + fi + + # Globally assign fuzzy_version to this value + # Use this value as the return value of this function + declare -g ${version_variable_name}="=${fuzzy_version}" + echo "${version_variable_name} ${!version_variable_name}" + + # Globally assign package to this value + # Use this value as the return value of this function + declare -g ${package_variable_name}="${package_name}" + echo "${package_variable_name} ${!package_variable_name}" +} + +# Install .NET CLI using apt-get package installer +install_using_apt() { + local sdk_or_runtime="$1" + local dotnet_major_minor_version + export DOTNET_PACKAGE="dotnet-${sdk_or_runtime}" + + # Install dependencies + check_packages apt-transport-https curl ca-certificates gnupg2 dirmngr + + # Import key safely and import Microsoft apt repo + get_common_setting MICROSOFT_GPG_KEYS_URI + curl -sSL ${MICROSOFT_GPG_KEYS_URI} | gpg --dearmor > /usr/share/keyrings/microsoft-archive-keyring.gpg + echo "deb [arch=${architecture} signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/repos/microsoft-${ID}-${VERSION_CODENAME}-prod ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/microsoft.list + apt-get update + + if [ "${DOTNET_VERSION}" = "latest" ] || [ "${DOTNET_VERSION}" = "lts" ]; then + DOTNET_VERSION="" + DOTNET_PACKAGE="${DOTNET_PACKAGE}-6.0" + else + # Sets DOTNET_VERSION and DOTNET_PACKAGE if matches found. + apt_cache_package_and_version_soft_match DOTNET_VERSION DOTNET_PACKAGE false + if [ "$?" != 0 ]; then + return 1 + fi + fi + + if ! (apt-get install -yq ${DOTNET_PACKAGE}${DOTNET_VERSION}); then + return 1 + fi +} + +# Find and extract .NET binary download details based on user-requested version +# args: +# sdk_or_runtime $1 +# exports: +# DOTNET_DOWNLOAD_URL +# DOTNET_DOWNLOAD_HASH +# DOTNET_DOWNLOAD_NAME +get_full_version_details() { + local sdk_or_runtime="$1" + local architecture + local dotnet_channel_version + local dotnet_releases_url + local dotnet_releases_json + local dotnet_latest_version + local dotnet_download_details + + export DOTNET_DOWNLOAD_URL + export DOTNET_DOWNLOAD_HASH + export DOTNET_DOWNLOAD_NAME + + # Set architecture variable to current user's architecture (x64 or ARM64). + architecture="$(get_architecture_name_for_target_os)" + + # Set DOTNET_VERSION to empty string to ensure jq includes all .NET versions in reverse sort below + if [ "${DOTNET_VERSION}" = "latest" ]; then + DOTNET_VERSION="" + fi + + dotnet_patchless_version="$(echo "${DOTNET_VERSION}" | cut -d "." --field=1,2)" + + set +e + dotnet_channel_version="$(curl -s "${DOTNET_CDN_FEED_URI}/dotnet/release-metadata/releases-index.json" | jq -r --arg channel_version "${dotnet_patchless_version}" '[."releases-index"[]] | sort_by(."channel-version") | reverse | map( select(."channel-version" | startswith($channel_version))) | first | ."channel-version"')" + set -e + + # Construct the releases URL using the official channel-version if one was found. Otherwise make a best-effort using the user input. + if [ -n "${dotnet_channel_version}" ] && [ "${dotnet_channel_version}" != "null" ]; then + dotnet_releases_url="${DOTNET_CDN_FEED_URI}/dotnet/release-metadata/${dotnet_channel_version}/releases.json" + else + dotnet_releases_url="${DOTNET_CDN_FEED_URI}/dotnet/release-metadata/${dotnet_patchless_version}/releases.json" + fi + + set +e + dotnet_releases_json="$(curl -s "${dotnet_releases_url}")" + set -e + + if [ -n "${dotnet_releases_json}" ] && [[ ! "${dotnet_releases_json}" = *"Error"* ]]; then + dotnet_latest_version="$(echo "${dotnet_releases_json}" | jq -r --arg sdk_or_runtime "${sdk_or_runtime}" '."latest-\($sdk_or_runtime)"')" + # If user-specified version has 2 or more dots, use it as is. Otherwise use latest version. + if [ "$(echo "${DOTNET_VERSION}" | grep -o "\." | wc -l)" -lt "2" ]; then + DOTNET_VERSION="${dotnet_latest_version}" + fi + + dotnet_download_details="$(echo "${dotnet_releases_json}" | jq -r --arg sdk_or_runtime "${sdk_or_runtime}" --arg dotnet_version "${DOTNET_VERSION}" --arg arch "${architecture}" '.releases[]."\($sdk_or_runtime)" | select(.version==$dotnet_version) | .files[] | select(.name=="dotnet-\($sdk_or_runtime)-linux-\($arch).tar.gz")')" + if [ -n "${dotnet_download_details}" ]; then + echo "Found .NET binary version ${DOTNET_VERSION}" + DOTNET_DOWNLOAD_URL="$(echo "${dotnet_download_details}" | jq -r '.url')" + DOTNET_DOWNLOAD_HASH="$(echo "${dotnet_download_details}" | jq -r '.hash')" + DOTNET_DOWNLOAD_NAME="$(echo "${dotnet_download_details}" | jq -r '.name')" + else + err "Unable to find .NET binary for version ${DOTNET_VERSION}" + exit 1 + fi + else + err "Unable to find .NET release details for version ${DOTNET_VERSION} at ${dotnet_releases_url}" + exit 1 + fi +} + +# Install .NET CLI using the .NET releases url +install_using_dotnet_releases_url() { + local sdk_or_runtime="$1" + + # Check listed package dependecies and install them if they are not already installed. + # NOTE: icu-devtools is a small package with similar dependecies to .NET. + # It will install the appropriate dependencies based on the OS: + # - libgcc-s1 OR libgcc1 depending on OS + # - the latest libicuXX depending on OS (eg libicu57 for stretch) + # - also installs libc6 and libstdc++6 which are required by .NET + check_packages curl ca-certificates tar jq icu-devtools libgssapi-krb5-2 libssl1.1 zlib1g + + get_full_version_details "${sdk_or_runtime}" + # exports DOTNET_DOWNLOAD_URL, DOTNET_DOWNLOAD_HASH, DOTNET_DOWNLOAD_NAME + echo "DOWNLOAD LINK: ${DOTNET_DOWNLOAD_URL}" + + # Setup the access group and add the user to it. + umask 0002 + if ! cat /etc/group | grep -e "^${ACCESS_GROUP}:" > /dev/null 2>&1; then + groupadd -r "${ACCESS_GROUP}" + fi + usermod -a -G "${ACCESS_GROUP}" "${USERNAME}" + + # Download the .NET binaries. + echo "DOWNLOADING BINARY..." + TMP_DIR="/tmp/dotnetinstall" + mkdir -p "${TMP_DIR}" + curl -sSL "${DOTNET_DOWNLOAD_URL}" -o "${TMP_DIR}/${DOTNET_DOWNLOAD_NAME}" + + # Get checksum from .NET CLI blob storage using the runtime version and + # run validation (sha512sum) of checksum against the expected checksum hash. + echo "VERIFY CHECKSUM" + cd "${TMP_DIR}" + echo "${DOTNET_DOWNLOAD_HASH} *${DOTNET_DOWNLOAD_NAME}" | sha512sum -c - + + # Extract binaries and add to path. + mkdir -p "${TARGET_DOTNET_ROOT}" + echo "Extract Binary to ${TARGET_DOTNET_ROOT}" + tar -xzf "${TMP_DIR}/${DOTNET_DOWNLOAD_NAME}" -C "${TARGET_DOTNET_ROOT}" --strip-components=1 + + updaterc "$(cat << EOF + export DOTNET_ROOT="${TARGET_DOTNET_ROOT}" + if [[ "\${PATH}" != *"\${DOTNET_ROOT}"* ]]; then export PATH="\${PATH}:\${DOTNET_ROOT}"; fi +EOF + )" + + # Give write permissions to the user. + chown -R ":${ACCESS_GROUP}" "${TARGET_DOTNET_ROOT}" + chmod g+r+w+s "${TARGET_DOTNET_ROOT}" + chmod -R g+r+w "${TARGET_DOTNET_ROOT}" +} + +########################### +# Start .NET installation +########################### + +export DEBIAN_FRONTEND=noninteractive + +# Determine if the user wants to download .NET Runtime only, or .NET SDK & Runtime +# and set the appropriate variables. +if [ "${DOTNET_RUNTIME_ONLY}" = "true" ]; then + DOTNET_SDK_OR_RUNTIME="runtime" +elif [ "${DOTNET_RUNTIME_ONLY}" = "false" ]; then + DOTNET_SDK_OR_RUNTIME="sdk" +else + err "Expected true for installing dotnet Runtime only or false for installing SDK and Runtime. Received ${DOTNET_RUNTIME_ONLY}." + exit 1 +fi + +# Install the .NET CLI +echo "(*) Installing .NET CLI..." + +. /etc/os-release +architecture="$(dpkg --print-architecture)" + +use_dotnet_releases_url="false" +if [[ "${DOTNET_ARCHIVE_ARCHITECTURES}" = *"${architecture}"* ]] && [[ "${DOTNET_ARCHIVE_VERSION_CODENAMES}" = *"${VERSION_CODENAME}"* ]]; then + install_using_apt "${DOTNET_SDK_OR_RUNTIME}" || use_dotnet_releases_url="true" +else + use_dotnet_releases_url="true" +fi + +if [ "${use_dotnet_releases_url}" = "true" ]; then + install_using_dotnet_releases_url "${DOTNET_SDK_OR_RUNTIME}" +fi + +echo "Done!" \ No newline at end of file diff --git a/collection/git-lfs/feature.json b/collection/git-lfs/feature.json new file mode 100644 index 0000000..4fa33c5 --- /dev/null +++ b/collection/git-lfs/feature.json @@ -0,0 +1,16 @@ +{ + "id": "git-lfs", + "name": "Git Large File Support (LFS)", + "options": { + "version": { + "type": "string", + "enum": ["latest"], + "default": "latest", + "description": "Currently unused." + } + }, + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/git-lfs/install.sh b/collection/git-lfs/install.sh new file mode 100644 index 0000000..dd9e702 --- /dev/null +++ b/collection/git-lfs/install.sh @@ -0,0 +1,194 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/git-lfs.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./git-lfs-debian.sh [version] + +GIT_LFS_VERSION=${1:-"latest"} +GIT_LFS_ARCHIVE_GPG_KEY_URI="https://packagecloud.io/github/git-lfs/gpgkey" +GIT_LFS_ARCHIVE_ARCHITECTURES="amd64" +GIT_LFS_ARCHIVE_VERSION_CODENAMES="stretch buster bullseye bionic focal" +GIT_LFS_CHECKSUM_GPG_KEYS="0x88ace9b29196305ba9947552f1ba225c0223b187 0x86cd3297749375bcf8206715f54fe648088335a9 0xaa3b3450295830d2de6db90caba67be5a5795889" +GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com:80 +keyserver hkps://keys.openpgp.org +keyserver hkp://keyserver.pgp.com" + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Figure out correct version of a three part version number is not passed +find_version_from_git_tags() { + local variable_name=$1 + local requested_version=${!variable_name} + if [ "${requested_version}" = "none" ]; then return; fi + local repository=$2 + local prefix=${3:-"tags/v"} + local separator=${4:-"."} + local last_part_optional=${5:-"false"} + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local escaped_separator=${separator//./\\.} + local last_part + if [ "${last_part_optional}" = "true" ]; then + last_part="(${escaped_separator}[0-9]+)?" + else + last_part="${escaped_separator}[0-9]+" + fi + local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" + local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" + else + set +e + declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + fi + if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then + echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 + exit 1 + fi + echo "${variable_name}=${!variable_name}" +} + +# Import the specified key in a variable name passed in as +receive_gpg_keys() { + get_common_setting $1 + local keys=${!1} + get_common_setting GPG_KEY_SERVERS true + + # Use a temporary locaiton for gpg keys to avoid polluting image + export GNUPGHOME="/tmp/tmp-gnupg" + mkdir -p ${GNUPGHOME} + chmod 700 ${GNUPGHOME} + echo -e "disable-ipv6\n${GPG_KEY_SERVERS}" > ${GNUPGHOME}/dirmngr.conf + # GPG key download sometimes fails for some reason and retrying fixes it. + local retry_count=0 + local gpg_ok="false" + set +e + until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; + do + echo "(*) Downloading GPG key..." + ( echo "${keys}" | xargs -n 1 gpg --recv-keys) 2>&1 && gpg_ok="true" + if [ "${gpg_ok}" != "true" ]; then + echo "(*) Failed getting key, retring in 10s..." + (( retry_count++ )) + sleep 10s + fi + done + set -e + if [ "${gpg_ok}" = "false" ]; then + echo "(!) Failed to get gpg key." + exit 1 + fi +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +install_using_apt() { + # Soft version matching + if [ "${GIT_LFS_VERSION}" != "latest" ] && [ "${GIT_LFS_VERSION}" != "lts" ] && [ "${GIT_LFS_VERSION}" != "stable" ]; then + find_version_from_git_tags GIT_LFS_VERSION "https://github.com/git-lfs/git-lfs" + version_suffix="=${GIT_LFS_VERSION}" + else + version_suffix="" + fi + # Install + get_common_setting GIT_LFS_ARCHIVE_GPG_KEY_URI + curl -sSL "${GIT_LFS_ARCHIVE_GPG_KEY_URI}" | gpg --dearmor > /usr/share/keyrings/gitlfs-archive-keyring.gpg + echo -e "deb [arch=${architecture} signed-by=/usr/share/keyrings/gitlfs-archive-keyring.gpg] https://packagecloud.io/github/git-lfs/${ID} ${VERSION_CODENAME} main\ndeb-src [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gitlfs-archive-keyring.gpg] https://packagecloud.io/github/git-lfs/${ID} ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/git-lfs.list + + if ! (apt-get update && apt-get install -yq git-lfs${version_suffix}); then + rm -f /etc/apt/sources.list.d/git-lfs.list + return 1 + fi + + git-lfs install --skip-repo +} + +install_using_github() { + echo "(*) No apt package for ${VERSION_CODENAME} ${architecture}. Installing manually." + mkdir -p /tmp/git-lfs + cd /tmp/git-lfs + find_version_from_git_tags GIT_LFS_VERSION "https://github.com/git-lfs/git-lfs" + git_lfs_filename="git-lfs-linux-${architecture}-v${GIT_LFS_VERSION}.tar.gz" + curl -sSL -o "${git_lfs_filename}" "https://github.com/git-lfs/git-lfs/releases/download/v${GIT_LFS_VERSION}/${git_lfs_filename}" + # Verify file + curl -sSL -o "sha256sums.asc" "https://github.com/git-lfs/git-lfs/releases/download/v${GIT_LFS_VERSION}/sha256sums.asc" + receive_gpg_keys GIT_LFS_CHECKSUM_GPG_KEYS + gpg -q --decrypt "sha256sums.asc" > sha256sums + sha256sum --ignore-missing -c "sha256sums" + # Extract and install + tar xf "${git_lfs_filename}" -C . + ./install.sh + rm -rf /tmp/git-lfs /tmp/tmp-gnupg +} + +export DEBIAN_FRONTEND=noninteractive + +# Install git, curl, gpg, dirmngr and debian-archive-keyring if missing +. /etc/os-release +check_packages curl ca-certificates gnupg2 dirmngr apt-transport-https +if ! type git > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends git +fi +if [ "${ID}" = "debian" ]; then + check_packages debian-archive-keyring +fi + +# Install Git LFS +echo "Installing Git LFS..." +architecture="$(dpkg --print-architecture)" +if [[ "${GIT_LFS_ARCHIVE_ARCHITECTURES}" = *"${architecture}"* ]] && [[ "${GIT_LFS_ARCHIVE_VERSION_CODENAMES}" = *"${VERSION_CODENAME}"* ]]; then + install_using_apt || use_github="true" +else + use_github="true" +fi + +# If no archive exists or apt install fails, try direct from github +if [ "${use_github}" = "true" ]; then + install_using_github +fi + +echo "Done!" \ No newline at end of file diff --git a/collection/git/feature.json b/collection/git/feature.json new file mode 100644 index 0000000..6d9686b --- /dev/null +++ b/collection/git/feature.json @@ -0,0 +1,21 @@ +{ + "id": "git", + "name": "Git (may require compilation)", + "options": { + "version": { + "type": "string", + "proposals": ["latest", "os-provided"], + "default": "os-provided", + "description": "Select or enter a Git version." + }, + "ppa": { + "type": "boolean", + "default": true, + "description": "Install from PPA if available" + } + }, + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/git/install.sh b/collection/git/install.sh new file mode 100644 index 0000000..6900897 --- /dev/null +++ b/collection/git/install.sh @@ -0,0 +1,153 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/git-from-src.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./git-from-src-debian.sh [version] [use PPA if available] + +GIT_VERSION=${1:-"latest"} # 'system' checks the base image first, else installs 'latest' +USE_PPA_IF_AVAILABLE=${2:-"false"} + +GIT_CORE_PPA_ARCHIVE_GPG_KEY=E1DD270288B4E6030699E45FA1715D88E1DF1F24 +GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com:80 +keyserver hkps://keys.openpgp.org +keyserver hkp://keyserver.pgp.com" + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Import the specified key in a variable name passed in as +receive_gpg_keys() { + get_common_setting $1 + local keys=${!1} + get_common_setting GPG_KEY_SERVERS true + local keyring_args="" + if [ ! -z "$2" ]; then + mkdir -p "$(dirname \"$2\")" + keyring_args="--no-default-keyring --keyring $2" + fi + + # Use a temporary locaiton for gpg keys to avoid polluting image + export GNUPGHOME="/tmp/tmp-gnupg" + mkdir -p ${GNUPGHOME} + chmod 700 ${GNUPGHOME} + echo -e "disable-ipv6\n${GPG_KEY_SERVERS}" > ${GNUPGHOME}/dirmngr.conf + # GPG key download sometimes fails for some reason and retrying fixes it. + local retry_count=0 + local gpg_ok="false" + set +e + until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; + do + echo "(*) Downloading GPG key..." + ( echo "${keys}" | xargs -n 1 gpg -q ${keyring_args} --recv-keys) 2>&1 && gpg_ok="true" + if [ "${gpg_ok}" != "true" ]; then + echo "(*) Failed getting key, retring in 10s..." + (( retry_count++ )) + sleep 10s + fi + done + set -e + if [ "${gpg_ok}" = "false" ]; then + echo "(!) Failed to get gpg key." + exit 1 + fi +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +export DEBIAN_FRONTEND=noninteractive + +# Source /etc/os-release to get OS info +. /etc/os-release + +# If the os provided version is "good enough", just install that. +if [ ${GIT_VERSION} = "os-provided" ] || [ ${GIT_VERSION} = "system" ]; then + if type git > /dev/null 2>&1; then + echo "Detected existing system install: $(git version)" + exit 0 + fi + + echo "Installing git from OS apt repository" + check_packages git + exit 0 +fi + +# If ubuntu, PPAs allowed, and latest - install from there +if ([ "${GIT_VERSION}" = "latest" ] || [ "${GIT_VERSION}" = "lts" ] || [ "${GIT_VERSION}" = "current" ]) && [ "${ID}" = "ubuntu" ] && [ "${USE_PPA_IF_AVAILABLE}" = "true" ]; then + echo "Using PPA to install latest git..." + check_packages apt-transport-https curl ca-certificates gnupg2 dirmngr + receive_gpg_keys GIT_CORE_PPA_ARCHIVE_GPG_KEY /usr/share/keyrings/gitcoreppa-archive-keyring.gpg + echo -e "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gitcoreppa-archive-keyring.gpg] http://ppa.launchpad.net/git-core/ppa/ubuntu ${VERSION_CODENAME} main\ndeb-src [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gitcoreppa-archive-keyring.gpg] http://ppa.launchpad.net/git-core/ppa/ubuntu ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/git-core-ppa.list + apt-get update + apt-get -y install --no-install-recommends git + rm -rf "/tmp/tmp-gnupg" + exit 0 +fi + +# Install required packages to build if missing +check_packages build-essential curl ca-certificates tar gettext libssl-dev zlib1g-dev libcurl?-openssl-dev libexpat1-dev + +# Partial version matching +if [ "$(echo "${GIT_VERSION}" | grep -o '\.' | wc -l)" != "2" ]; then + requested_version="${GIT_VERSION}" + version_list="$(curl -sSL -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/git/git/tags" | grep -oP '"name":\s*"v\K[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"' | sort -rV )" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "lts" ] || [ "${requested_version}" = "current" ]; then + GIT_VERSION="$(echo "${version_list}" | head -n 1)" + else + set +e + GIT_VERSION="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + if [ -z "${GIT_VERSION}" ] || ! echo "${version_list}" | grep "^${GIT_VERSION//./\\.}$" > /dev/null 2>&1; then + echo "Invalid git version: ${requested_version}" >&2 + exit 1 + fi +fi + +echo "Downloading source for ${GIT_VERSION}..." +curl -sL https://github.com/git/git/archive/v${GIT_VERSION}.tar.gz | tar -xzC /tmp 2>&1 +echo "Building..." +cd /tmp/git-${GIT_VERSION} +make -s prefix=/usr/local all && make -s prefix=/usr/local install 2>&1 +rm -rf /tmp/git-${GIT_VERSION} +echo "Done!" diff --git a/collection/github-cli/feature.json b/collection/github-cli/feature.json new file mode 100644 index 0000000..03f07cf --- /dev/null +++ b/collection/github-cli/feature.json @@ -0,0 +1,16 @@ +{ + "id": "github-cli", + "name": "GitHub CLI", + "options": { + "version": { + "type": "string", + "proposals": ["latest"], + "default": "latest", + "description": "Select version of the GitHub CLI, if not latest." + } + }, + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/github-cli/install.sh b/collection/github-cli/install.sh new file mode 100644 index 0000000..01ad4d4 --- /dev/null +++ b/collection/github-cli/install.sh @@ -0,0 +1,188 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/github.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./github-debian.sh [version] + +CLI_VERSION=${1:-"latest"} + +GITHUB_CLI_ARCHIVE_GPG_KEY=C99B11DEB97541F0 +GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com:80 +keyserver hkps://keys.openpgp.org +keyserver hkp://keyserver.pgp.com" + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Import the specified key in a variable name passed in as +receive_gpg_keys() { + get_common_setting $1 + local keys=${!1} + get_common_setting GPG_KEY_SERVERS true + + # Use a temporary locaiton for gpg keys to avoid polluting image + export GNUPGHOME="/tmp/tmp-gnupg" + mkdir -p ${GNUPGHOME} + chmod 700 ${GNUPGHOME} + echo -e "disable-ipv6\n${GPG_KEY_SERVERS}" > ${GNUPGHOME}/dirmngr.conf + # GPG key download sometimes fails for some reason and retrying fixes it. + local retry_count=0 + local gpg_ok="false" + set +e + until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; + do + echo "(*) Downloading GPG key..." + ( echo "${keys}" | xargs -n 1 gpg --recv-keys) 2>&1 && gpg_ok="true" + if [ "${gpg_ok}" != "true" ]; then + echo "(*) Failed getting key, retring in 10s..." + (( retry_count++ )) + sleep 10s + fi + done + set -e + if [ "${gpg_ok}" = "false" ]; then + echo "(!) Failed to get gpg key." + exit 1 + fi +} + +# Figure out correct version of a three part version number is not passed +find_version_from_git_tags() { + local variable_name=$1 + local requested_version=${!variable_name} + if [ "${requested_version}" = "none" ]; then return; fi + local repository=$2 + local prefix=${3:-"tags/v"} + local separator=${4:-"."} + local last_part_optional=${5:-"false"} + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local escaped_separator=${separator//./\\.} + local last_part + if [ "${last_part_optional}" = "true" ]; then + last_part="(${escaped_separator}[0-9]+)?" + else + last_part="${escaped_separator}[0-9]+" + fi + local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" + local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" + else + set +e + declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + fi + if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then + echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 + exit 1 + fi + echo "${variable_name}=${!variable_name}" +} + +# Import the specified key in a variable name passed in as +receive_gpg_keys() { + get_common_setting $1 + local keys=${!1} + get_common_setting GPG_KEY_SERVERS true + local keyring_args="" + if [ ! -z "$2" ]; then + keyring_args="--no-default-keyring --keyring $2" + fi + + # Use a temporary locaiton for gpg keys to avoid polluting image + export GNUPGHOME="/tmp/tmp-gnupg" + mkdir -p ${GNUPGHOME} + chmod 700 ${GNUPGHOME} + echo -e "disable-ipv6\n${GPG_KEY_SERVERS}" > ${GNUPGHOME}/dirmngr.conf + # GPG key download sometimes fails for some reason and retrying fixes it. + local retry_count=0 + local gpg_ok="false" + set +e + until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; + do + echo "(*) Downloading GPG key..." + ( echo "${keys}" | xargs -n 1 gpg -q ${keyring_args} --recv-keys) 2>&1 && gpg_ok="true" + if [ "${gpg_ok}" != "true" ]; then + echo "(*) Failed getting key, retring in 10s..." + (( retry_count++ )) + sleep 10s + fi + done + set -e + if [ "${gpg_ok}" = "false" ]; then + echo "(!) Failed to get gpg key." + exit 1 + fi +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +export DEBIAN_FRONTEND=noninteractive + +# Install curl, apt-transport-https, curl, gpg, or dirmngr, git if missing +check_packages curl ca-certificates apt-transport-https dirmngr gnupg2 +if ! type git > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends git +fi + +# Soft version matching +if [ "${CLI_VERSION}" != "latest" ] && [ "${CLI_VERSION}" != "lts" ] && [ "${CLI_VERSION}" != "stable" ]; then + find_version_from_git_tags CLI_VERSION "https://github.com/cli/cli" + version_suffix="=${CLI_VERSION}" +else + version_suffix="" +fi + +# Install the GitHub CLI +echo "Downloading github CLI..." +# Import key safely (new method rather than deprecated apt-key approach) and install +. /etc/os-release +receive_gpg_keys GITHUB_CLI_ARCHIVE_GPG_KEY /usr/share/keyrings/githubcli-archive-keyring.gpg +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list +apt-get update +apt-get -y install "gh${version_suffix}" +rm -rf "/tmp/gh/gnupg" +echo "Done!" diff --git a/collection/go/feature.json b/collection/go/feature.json new file mode 100644 index 0000000..0f8c6db --- /dev/null +++ b/collection/go/feature.json @@ -0,0 +1,24 @@ +{ + "id": "go", + "name": "Go", + "options": { + "version": { + "type": "string", + "proposals": ["latest", "1.18", "1.17"], + "default": "latest", + "description": "Select or enter a Go version to install" + } + }, + "init": true, + "extensions": ["golang.Go"], + "containerEnv": { + "GOPATH": "/go", + "PATH": "${GOPATH}/bin:${GOROOT}/bin:${PATH}" + }, + "capAdd": [ "SYS_PTRACE" ], + "securityOpt": [ "seccomp=unconfined" ], + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/go/install.sh b/collection/go/install.sh new file mode 100644 index 0000000..47ab299 --- /dev/null +++ b/collection/go/install.sh @@ -0,0 +1,255 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/go.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./go-debian.sh [Go version] [GOROOT] [GOPATH] [non-root user] [Add GOPATH, GOROOT to rc files flag] [Install tools flag] + +TARGET_GO_VERSION=${1:-"latest"} +TARGET_GOROOT=${2:-"/usr/local/go"} +TARGET_GOPATH=${3:-"/go"} +USERNAME=${4:-"automatic"} +UPDATE_RC=${5:-"true"} +INSTALL_GO_TOOLS=${6:-"true"} + +# https://www.google.com/linuxrepositories/ +GO_GPG_KEY_URI="https://dl.google.com/linux/linux_signing_key.pub" + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +updaterc() { + if [ "${UPDATE_RC}" = "true" ]; then + echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." + if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/bash.bashrc + fi + if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/zsh/zshrc + fi + fi +} +# Figure out correct version of a three part version number is not passed +find_version_from_git_tags() { + local variable_name=$1 + local requested_version=${!variable_name} + if [ "${requested_version}" = "none" ]; then return; fi + local repository=$2 + local prefix=${3:-"tags/v"} + local separator=${4:-"."} + local last_part_optional=${5:-"false"} + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local escaped_separator=${separator//./\\.} + local last_part + if [ "${last_part_optional}" = "true" ]; then + last_part="(${escaped_separator}[0-9]+)?" + else + last_part="${escaped_separator}[0-9]+" + fi + local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" + local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" + else + set +e + declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + fi + if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then + echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 + exit 1 + fi + echo "${variable_name}=${!variable_name}" +} + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +export DEBIAN_FRONTEND=noninteractive + +# Install curl, tar, git, other dependencies if missing +check_packages curl ca-certificates gnupg2 tar g++ gcc libc6-dev make pkg-config +if ! type git > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends git +fi + +# Get closest match for version number specified +find_version_from_git_tags TARGET_GO_VERSION "https://go.googlesource.com/go" "tags/go" "." "true" + +architecture="$(uname -m)" +case $architecture in + x86_64) architecture="amd64";; + aarch64 | armv8*) architecture="arm64";; + aarch32 | armv7* | armvhf*) architecture="armv6l";; + i?86) architecture="386";; + *) echo "(!) Architecture $architecture unsupported"; exit 1 ;; +esac + +# Install Go +umask 0002 +if ! cat /etc/group | grep -e "^golang:" > /dev/null 2>&1; then + groupadd -r golang +fi +usermod -a -G golang "${USERNAME}" +mkdir -p "${TARGET_GOROOT}" "${TARGET_GOPATH}" +if [ "${TARGET_GO_VERSION}" != "none" ] && ! type go > /dev/null 2>&1; then + # Use a temporary locaiton for gpg keys to avoid polluting image + export GNUPGHOME="/tmp/tmp-gnupg" + mkdir -p ${GNUPGHOME} + chmod 700 ${GNUPGHOME} + get_common_setting GO_GPG_KEY_URI + curl -sSL -o /tmp/tmp-gnupg/golang_key "${GO_GPG_KEY_URI}" + gpg -q --import /tmp/tmp-gnupg/golang_key + echo "Downloading Go ${TARGET_GO_VERSION}..." + set +e + curl -fsSL -o /tmp/go.tar.gz "https://golang.org/dl/go${TARGET_GO_VERSION}.linux-${architecture}.tar.gz" + exit_code=$? + set -e + if [ "$exit_code" != "0" ]; then + echo "(!) Download failed." + # Try one break fix version number less if we get a failure. Use "set +e" since "set -e" can cause failures in valid scenarios. + set +e + major="$(echo "${TARGET_GO_VERSION}" | grep -oE '^[0-9]+' || echo '')" + minor="$(echo "${TARGET_GO_VERSION}" | grep -oP '^[0-9]+\.\K[0-9]+' || echo '')" + breakfix="$(echo "${TARGET_GO_VERSION}" | grep -oP '^[0-9]+\.[0-9]+\.\K[0-9]+' 2>/dev/null || echo '')" + # Handle Go's odd version pattern where "0" releases omit the last part + if [ "${breakfix}" = "" ] || [ "${breakfix}" = "0" ]; then + ((minor=minor-1)) + TARGET_GO_VERSION="${major}.${minor}" + # Look for latest version from previous minor release + find_version_from_git_tags TARGET_GO_VERSION "https://go.googlesource.com/go" "tags/go" "." "true" + else + ((breakfix=breakfix-1)) + if [ "${breakfix}" = "0" ]; then + TARGET_GO_VERSION="${major}.${minor}" + else + TARGET_GO_VERSION="${major}.${minor}.${breakfix}" + fi + fi + set -e + echo "Trying ${TARGET_GO_VERSION}..." + curl -fsSL -o /tmp/go.tar.gz "https://golang.org/dl/go${TARGET_GO_VERSION}.linux-${architecture}.tar.gz" + fi + curl -fsSL -o /tmp/go.tar.gz.asc "https://golang.org/dl/go${TARGET_GO_VERSION}.linux-${architecture}.tar.gz.asc" + gpg --verify /tmp/go.tar.gz.asc /tmp/go.tar.gz + echo "Extracting Go ${TARGET_GO_VERSION}..." + tar -xzf /tmp/go.tar.gz -C "${TARGET_GOROOT}" --strip-components=1 + rm -rf /tmp/go.tar.gz /tmp/go.tar.gz.asc /tmp/tmp-gnupg +else + echo "Go already installed. Skipping." +fi + +# Install Go tools that are isImportant && !replacedByGopls based on +# https://github.com/golang/vscode-go/blob/v0.31.1/src/goToolsInformation.ts +GO_TOOLS="\ + golang.org/x/tools/gopls@latest \ + honnef.co/go/tools/cmd/staticcheck@latest \ + golang.org/x/lint/golint@latest \ + github.com/mgechev/revive@latest \ + github.com/uudashr/gopkgs/v2/cmd/gopkgs@latest \ + github.com/ramya-rao-a/go-outline@latest \ + github.com/go-delve/delve/cmd/dlv@latest \ + github.com/golangci/golangci-lint/cmd/golangci-lint@latest" +if [ "${INSTALL_GO_TOOLS}" = "true" ]; then + echo "Installing common Go tools..." + export PATH=${TARGET_GOROOT}/bin:${PATH} + mkdir -p /tmp/gotools /usr/local/etc/vscode-dev-containers ${TARGET_GOPATH}/bin + cd /tmp/gotools + export GOPATH=/tmp/gotools + export GOCACHE=/tmp/gotools/cache + + # Use go get for versions of go under 1.16 + go_install_command=install + if [[ "1.16" > "$(go version | grep -oP 'go\K[0-9]+\.[0-9]+(\.[0-9]+)?')" ]]; then + export GO111MODULE=on + go_install_command=get + echo "Go version < 1.16, using go get." + fi + + (echo "${GO_TOOLS}" | xargs -n 1 go ${go_install_command} -v )2>&1 | tee -a /usr/local/etc/vscode-dev-containers/go.log + + # Move Go tools into path and clean up + mv /tmp/gotools/bin/* ${TARGET_GOPATH}/bin/ + + rm -rf /tmp/gotools +fi + +# Add GOPATH variable and bin directory into PATH in bashrc/zshrc files (unless disabled) +updaterc "$(cat << EOF +export GOPATH="${TARGET_GOPATH}" +if [[ "\${PATH}" != *"\${GOPATH}/bin"* ]]; then export PATH="\${PATH}:\${GOPATH}/bin"; fi +export GOROOT="${TARGET_GOROOT}" +if [[ "\${PATH}" != *"\${GOROOT}/bin"* ]]; then export PATH="\${PATH}:\${GOROOT}/bin"; fi +EOF +)" + +chown -R :golang "${TARGET_GOROOT}" "${TARGET_GOPATH}" +chmod -R g+r+w "${TARGET_GOROOT}" "${TARGET_GOPATH}" +find "${TARGET_GOROOT}" -type d | xargs -n 1 chmod g+s +find "${TARGET_GOPATH}" -type d | xargs -n 1 chmod g+s + +echo "Done!" + diff --git a/collection/gradle/feature.json b/collection/gradle/feature.json new file mode 100644 index 0000000..3a393b9 --- /dev/null +++ b/collection/gradle/feature.json @@ -0,0 +1,22 @@ +{ + "id": "gradle", + "name": "Gradle (via SDKMAN!)", + "documentationURL": "https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/gradle.md", + "options": { + "version": { + "type": "string", + "proposals": ["latest", "7", "6", "5"], + "default": "latest", + "description": "Select or enter a Gradle version to install" + } + }, + "extensions": ["vscjava.vscode-java-pack"], + "containerEnv": { + "SDKMAN_DIR": "/usr/local/sdkman", + "PATH": "${SDKMAN_DIR}/bin:${SDKMAN_DIR}/candidates/gradle/current/bin:${PATH}" + }, + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/gradle/install.sh b/collection/gradle/install.sh new file mode 100644 index 0000000..e69de29 diff --git a/collection/hugo/feature.json b/collection/hugo/feature.json new file mode 100644 index 0000000..0b91ac1 --- /dev/null +++ b/collection/hugo/feature.json @@ -0,0 +1,16 @@ +{ + "id": "hugo", + "name": "Hugo", + "options": { + "version": { + "type": "string", + "proposals": ["latest"], + "default": "latest", + "description": "Select or enter a version." + } + }, + "install": { + "app": "", + "file": "install.sh" + } +} diff --git a/collection/hugo/install.sh b/collection/hugo/install.sh new file mode 100644 index 0000000..3913c7a --- /dev/null +++ b/collection/hugo/install.sh @@ -0,0 +1,113 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/hugo.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./hugo-debian.sh [Hugo version] [HUGO_DIR] [Non-root user] [Add rc files flag] + +VERSION=${1:-"latest"} +export HUGO_DIR=${2:-"/usr/local/hugo"} +USERNAME=${3:-"automatic"} +UPDATE_RC=${4:-"true"} + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +architecture="$(uname -m)" +if [ "${architecture}" != "amd64" ] && [ "${architecture}" != "x86_64" ] && [ "${architecture}" != "arm64" ] && [ "${architecture}" != "aarch64" ]; then + echo "(!) Architecture $architecture unsupported" + exit 1 +fi + +updaterc() { + if [ "${UPDATE_RC}" = "true" ]; then + echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." + if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/bash.bashrc + fi + if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/zsh/zshrc + fi + fi +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Install dependencies +check_packages curl ca-certificates tar + +# Fetch latest version of Hugo if needed +if [ "${VERSION}" = "latest" ] || [ "${VERSION}" = "lts" ]; then + export VERSION=$(curl -s https://api.github.com/repos/gohugoio/hugo/releases/latest | grep "tag_name" | awk '{print substr($2, 3, length($2)-4)}') +fi + +# Install Hugo if it's missing +if ! hugo version &> /dev/null ; then + echo "Installing Hugo..." + installation_dir="$HUGO_DIR/bin" + mkdir -p "$installation_dir" + + # Install ARM or x86 version of hugo based on current machine architecture + if [ "$(uname -m)" == "aarch64" ]; then + arch="ARM64" + else + arch="64bit" + fi + + hugo_filename="hugo_${VERSION}_Linux-${arch}.tar.gz" + + curl -fsSLO --compressed "https://github.com/gohugoio/hugo/releases/download/v${VERSION}/${hugo_filename}" + tar -xzf "$hugo_filename" -C "$installation_dir" + rm "$hugo_filename" + + updaterc "export HUGO_DIR=${installation_dir}" +fi + +echo "Done!" diff --git a/collection/java/feature.json b/collection/java/feature.json new file mode 100644 index 0000000..263c2c8 --- /dev/null +++ b/collection/java/feature.json @@ -0,0 +1,26 @@ +{ + "id": "java", + "name": "Java (via SDKMAN!)", + "options": { + "version": { + "type": "string", + "proposals": ["lts", "latest", "17", "11", "8"], + "default": "lts", + "description": "Select or enter a Java version to install" + } + }, + "buildArg": "_VSC_INSTALL_JAVA", + "extensions": ["vscjava.vscode-java-pack"], + "containerEnv": { + "SDKMAN_DIR": "/usr/local/sdkman", + "PATH": "${SDKMAN_DIR}/bin:${SDKMAN_DIR}/candidates/java/current/bin:${PATH}" + }, + "settings": { + "java.home": "/extension-java-home", + "java.import.gradle.java.home": "/usr/local/sdkman/candidates/java/current" + }, + "install": { + "app": "", + "file": "install.sh" + } +} diff --git a/collection/java/install.sh b/collection/java/install.sh new file mode 100644 index 0000000..f5880cd --- /dev/null +++ b/collection/java/install.sh @@ -0,0 +1,145 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/java.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./java-debian.sh [JDK version] [SDKMAN_DIR] [non-root user] [Add to rc files flag] + +JAVA_VERSION=${1:-"lts"} +export SDKMAN_DIR=${2:-"/usr/local/sdkman"} +USERNAME=${3:-"automatic"} +UPDATE_RC=${4:-"true"} + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +updaterc() { + if [ "${UPDATE_RC}" = "true" ]; then + echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." + if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/bash.bashrc + fi + if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/zsh/zshrc + fi + fi +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Use SDKMAN to install something using a partial version match +sdk_install() { + local install_type=$1 + local requested_version=$2 + local prefix=$3 + local suffix="${4:-"\\s*"}" + local full_version_check=${5:-".*-[a-z]+"} + if [ "${requested_version}" = "none" ]; then return; fi + # Blank will install latest stable version SDKMAN has + if [ "${requested_version}" = "lts" ] || [ "${requested_version}" = "default" ]; then + requested_version="" + elif echo "${requested_version}" | grep -oE "${full_version_check}" > /dev/null 2>&1; then + echo "${requested_version}" + else + local regex="${prefix}\\K[0-9]+\\.[0-9]+\\.[0-9]+${suffix}" + local version_list="$(. ${SDKMAN_DIR}/bin/sdkman-init.sh && sdk list ${install_type} 2>&1 | grep -oP "${regex}" | tr -d ' ' | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ]; then + requested_version="$(echo "${version_list}" | head -n 1)" + else + set +e + requested_version="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + if [ -z "${requested_version}" ] || ! echo "${version_list}" | grep "^${requested_version//./\\.}$" > /dev/null 2>&1; then + echo -e "Version $2 not found. Available versions:\n${version_list}" >&2 + exit 1 + fi + fi + su ${USERNAME} -c "umask 0002 && . ${SDKMAN_DIR}/bin/sdkman-init.sh && sdk install ${install_type} ${requested_version} && sdk flush archives && sdk flush temp" +} + +export DEBIAN_FRONTEND=noninteractive + +architecture="$(uname -m)" +if [ "${architecture}" != "amd64" ] && [ "${architecture}" != "x86_64" ] && [ "${architecture}" != "arm64" ] && [ "${architecture}" != "aarch64" ]; then + echo "(!) Architecture $architecture unsupported" + exit 1 +fi + +# Install dependencies +check_packages curl ca-certificates zip unzip sed + +# Install sdkman if not installed +if [ ! -d "${SDKMAN_DIR}" ]; then + # Create sdkman group, dir, and set sticky bit + if ! cat /etc/group | grep -e "^sdkman:" > /dev/null 2>&1; then + groupadd -r sdkman + fi + usermod -a -G sdkman ${USERNAME} + umask 0002 + # Install SDKMAN + curl -sSL "https://get.sdkman.io?rcupdate=false" | bash + chown -R :sdkman ${SDKMAN_DIR} + find ${SDKMAN_DIR} -type d | xargs -d '\n' chmod g+s + # Add sourcing of sdkman into bashrc/zshrc files (unless disabled) + updaterc "export SDKMAN_DIR=${SDKMAN_DIR}\n. \${SDKMAN_DIR}/bin/sdkman-init.sh" +fi + +# Use Microsoft JDK for everything but JDK 8 +jdk_distro="ms" +if echo "${JAVA_VERSION}" | grep -E '^8([\s\.]|$)' > /dev/null 2>&1; then + jdk_distro="tem" +fi +if [ "${JAVA_VERSION}" = "lts" ]; then + JAVA_VERSION="17" +fi +sdk_install java ${JAVA_VERSION} "\\s*" "(\\.[a-z0-9]+)*-${jdk_distro}\\s*" ".*-[a-z]+$" + +echo "Done!" \ No newline at end of file diff --git a/collection/jekyll/feature.json b/collection/jekyll/feature.json new file mode 100644 index 0000000..852aa30 --- /dev/null +++ b/collection/jekyll/feature.json @@ -0,0 +1,16 @@ +{ + "id": "jekyll", + "name": "Jekyll", + "options": { + "version": { + "type": "string", + "proposals": ["latest"], + "default": "latest", + "description": "Select or enter a version." + } + }, + "install": { + "app": "", + "file": "install.sh" + } +} diff --git a/collection/jekyll/install.sh b/collection/jekyll/install.sh new file mode 100644 index 0000000..53efa82 --- /dev/null +++ b/collection/jekyll/install.sh @@ -0,0 +1,60 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/hugo.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./jekyll-debian.sh [Jekyll version] [Non-root user] [Add rc files flag] + +VERSION=${1:-"latest"} +USERNAME=${2:-"automatic"} + +set -e + +# If in automatic mode, determine if a user already exists, if not use codespace +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=codespace + fi +elif [ "${USERNAME}" = "none" ]; then + USERNAME=root + USER_UID=0 + USER_GID=0 +fi + +# Use sudo to run as non-root user is not already running +sudoUserIf() +{ + if [ "$(id -u)" -eq 0 ] && [ "${USERNAME}" != "root" ]; then + sudo -u ${USERNAME} "$@" + else + "$@" + fi +} + +# If we don't yet have Ruby installed, exit. +if ! /usr/local/rvm/rubies/default/bin/ruby --version > /dev/null ; then + echo "You need to install Ruby before installing Jekyll." + exit 1 +fi + +# If we don't already have Jekyll installed, install it now. +if ! jekyll --version > /dev/null ; then + echo "Installing Jekyll..." + if [ "${VERSION}" = "latest" ]; then + PATH="/usr/local/rvm/rubies/default/bin:${PATH}" /usr/local/rvm/rubies/default/bin/gem install jekyll + else + PATH="/usr/local/rvm/rubies/default/bin:${PATH}" /usr/local/rvm/rubies/default/bin/gem install jekyll -v "${VERSION}" + fi +fi diff --git a/collection/jupyterlab/feature.json b/collection/jupyterlab/feature.json new file mode 100644 index 0000000..e080339 --- /dev/null +++ b/collection/jupyterlab/feature.json @@ -0,0 +1,21 @@ +{ + "id": "jupyterlab", + "name": "Jupyter Lab", + "options": { + "version": { + "type": "string", + "proposals": ["latest", "3.6.2"], + "default": "latest", + "description": "Select or enter a jupyterlab version." + } + }, + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-toolsai.jupyter" + ], + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/jupyterlab/install.sh b/collection/jupyterlab/install.sh new file mode 100644 index 0000000..45267b0 --- /dev/null +++ b/collection/jupyterlab/install.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +set -e + +VERSION=${1:-"latest"} +USERNAME=${2:-"automatic"} + +# If in automatic mode, determine if a user already exists, if not use vscode +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=vscode + fi +elif [ "${USERNAME}" = "none" ]; then + USERNAME=root + USER_UID=0 + USER_GID=0 +fi + +# Use sudo to run as non-root user is not already running +sudoUserIf() +{ + if [ "$(id -u)" -eq 0 ] && [ "${USERNAME}" != "root" ]; then + sudo -u ${USERNAME} "$@" + else + "$@" + fi +} + +# If we don't yet have Python, install it now. +if ! python --version > /dev/null ; then + echo "You need to install Python before installing JupyterLab." + exit 1 +fi + +# If we don't already have JupyterLab installed, install it now. +if ! jupyter-lab --version > /dev/null ; then + echo "Installing JupyterLab..." + if [ "${VERSION}" = "latest" ]; then + pip install jupyterlab + else + pip install jupyterlab=="${VERSION}" --no-cache-dir + fi +fi diff --git a/collection/kubectl-helm-minikube/feature.json b/collection/kubectl-helm-minikube/feature.json new file mode 100644 index 0000000..18723c7 --- /dev/null +++ b/collection/kubectl-helm-minikube/feature.json @@ -0,0 +1,38 @@ +{ + "id": "kubectl-helm-minikube", + "name": "Kubectl, Helm, and Minkube", + "options": { + "version": { + "type": "string", + "proposals": ["latest", "1.23", "1.22", "1.21"], + "default": "latest", + "description": "Select or enter a Kubernetes version to install" + }, + "helm": { + "type": "string", + "proposals": ["latest"], + "default": "latest", + "description": "Select or enter a Helm version to install" + }, + "minikube": { + "type": "string", + "proposals": ["latest"], + "default": "latest", + "description": "Select or enter a Minikube version to install" + } + }, + "extensions": [ + "ms-kubernetes-tools.vscode-kubernetes-tools" + ], + "mounts": [ + { + "source":"minikube-config", + "target":"/home/vscode/.minikube", + "type":"volume" + } + ], + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/kubectl-helm-minikube/install.sh b/collection/kubectl-helm-minikube/install.sh new file mode 100644 index 0000000..0981024 --- /dev/null +++ b/collection/kubectl-helm-minikube/install.sh @@ -0,0 +1,249 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/kubectl-helm.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./kubectl-helm-debian.sh [kubectl verison] [Helm version] [minikube version] [kubectl SHA256] [Helm SHA256] [minikube SHA256] + +set -e + +KUBECTL_VERSION="${1:-"latest"}" +HELM_VERSION="${2:-"latest"}" +MINIKUBE_VERSION="${3:-"none"}" # latest is also valid +KUBECTL_SHA256="${4:-"automatic"}" +HELM_SHA256="${5:-"automatic"}" +MINIKUBE_SHA256="${6:-"automatic"}" +USERNAME=${7:-"automatic"} + +HELM_GPG_KEYS_URI="https://raw.githubusercontent.com/helm/helm/main/KEYS" +GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com:80 +keyserver hkps://keys.openpgp.org +keyserver hkp://keyserver.pgp.com" + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +USERHOME="/home/$USERNAME" +if [ "$USERNAME" = "root" ]; then + USERHOME="/root" +fi + + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Figure out correct version of a three part version number is not passed +find_version_from_git_tags() { + local variable_name=$1 + local requested_version=${!variable_name} + if [ "${requested_version}" = "none" ]; then return; fi + local repository=$2 + local prefix=${3:-"tags/v"} + local separator=${4:-"."} + local last_part_optional=${5:-"false"} + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local escaped_separator=${separator//./\\.} + local last_part + if [ "${last_part_optional}" = "true" ]; then + last_part="(${escaped_separator}[0-9]+)?" + else + last_part="${escaped_separator}[0-9]+" + fi + local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" + local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" + else + set +e + declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + fi + if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then + echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 + exit 1 + fi + echo "${variable_name}=${!variable_name}" +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Install dependencies +check_packages curl ca-certificates coreutils gnupg2 dirmngr bash-completion +if ! type git > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends git +fi + +architecture="$(uname -m)" +case $architecture in + x86_64) architecture="amd64";; + aarch64 | armv8*) architecture="arm64";; + aarch32 | armv7* | armvhf*) architecture="arm";; + i?86) architecture="386";; + *) echo "(!) Architecture $architecture unsupported"; exit 1 ;; +esac + +# Install the kubectl, verify checksum +echo "Downloading kubectl..." +if [ "${KUBECTL_VERSION}" = "latest" ] || [ "${KUBECTL_VERSION}" = "lts" ] || [ "${KUBECTL_VERSION}" = "current" ] || [ "${KUBECTL_VERSION}" = "stable" ]; then + KUBECTL_VERSION="$(curl -sSL https://dl.k8s.io/release/stable.txt)" +else + find_version_from_git_tags KUBECTL_VERSION https://github.com/kubernetes/kubernetes +fi +if [ "${KUBECTL_VERSION::1}" != 'v' ]; then + KUBECTL_VERSION="v${KUBECTL_VERSION}" +fi +curl -sSL -o /usr/local/bin/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${architecture}/kubectl" +chmod 0755 /usr/local/bin/kubectl +if [ "$KUBECTL_SHA256" = "automatic" ]; then + KUBECTL_SHA256="$(curl -sSL "https://dl.k8s.io/${KUBECTL_VERSION}/bin/linux/${architecture}/kubectl.sha256")" +fi +([ "${KUBECTL_SHA256}" = "dev-mode" ] || (echo "${KUBECTL_SHA256} */usr/local/bin/kubectl" | sha256sum -c -)) +if ! type kubectl > /dev/null 2>&1; then + echo '(!) kubectl installation failed!' + exit 1 +fi + +# kubectl bash completion +kubectl completion bash > /etc/bash_completion.d/kubectl + +# kubectl zsh completion +if [ -e "${USERHOME}}/.oh-my-zsh" ]; then + mkdir -p "${USERHOME}/.oh-my-zsh/completions" + kubectl completion zsh > "${USERHOME}/.oh-my-zsh/completions/_kubectl" + chown -R "${USERNAME}" "${USERHOME}/.oh-my-zsh" +fi + +# Install Helm, verify signature and checksum +echo "Downloading Helm..." +find_version_from_git_tags HELM_VERSION "https://github.com/helm/helm" +if [ "${HELM_VERSION::1}" != 'v' ]; then + HELM_VERSION="v${HELM_VERSION}" +fi +mkdir -p /tmp/helm +helm_filename="helm-${HELM_VERSION}-linux-${architecture}.tar.gz" +tmp_helm_filename="/tmp/helm/${helm_filename}" +curl -sSL "https://get.helm.sh/${helm_filename}" -o "${tmp_helm_filename}" +curl -sSL "https://github.com/helm/helm/releases/download/${HELM_VERSION}/${helm_filename}.asc" -o "${tmp_helm_filename}.asc" +export GNUPGHOME="/tmp/helm/gnupg" +mkdir -p "${GNUPGHOME}" +chmod 700 ${GNUPGHOME} +get_common_setting HELM_GPG_KEYS_URI +get_common_setting GPG_KEY_SERVERS true +curl -sSL "${HELM_GPG_KEYS_URI}" -o /tmp/helm/KEYS +echo -e "disable-ipv6\n${GPG_KEY_SERVERS}" > ${GNUPGHOME}/dirmngr.conf +gpg -q --import "/tmp/helm/KEYS" +if ! gpg --verify "${tmp_helm_filename}.asc" > ${GNUPGHOME}/verify.log 2>&1; then + echo "Verification failed!" + cat /tmp/helm/gnupg/verify.log + exit 1 +fi +if [ "${HELM_SHA256}" = "automatic" ]; then + curl -sSL "https://get.helm.sh/${helm_filename}.sha256" -o "${tmp_helm_filename}.sha256" + curl -sSL "https://github.com/helm/helm/releases/download/${HELM_VERSION}/${helm_filename}.sha256.asc" -o "${tmp_helm_filename}.sha256.asc" + if ! gpg --verify "${tmp_helm_filename}.sha256.asc" > /tmp/helm/gnupg/verify.log 2>&1; then + echo "Verification failed!" + cat /tmp/helm/gnupg/verify.log + exit 1 + fi + HELM_SHA256="$(cat "${tmp_helm_filename}.sha256")" +fi +([ "${HELM_SHA256}" = "dev-mode" ] || (echo "${HELM_SHA256} *${tmp_helm_filename}" | sha256sum -c -)) +tar xf "${tmp_helm_filename}" -C /tmp/helm +mv -f "/tmp/helm/linux-${architecture}/helm" /usr/local/bin/ +chmod 0755 /usr/local/bin/helm +rm -rf /tmp/helm +if ! type helm > /dev/null 2>&1; then + echo '(!) Helm installation failed!' + exit 1 +fi + +# Install Minikube, verify checksum +if [ "${MINIKUBE_VERSION}" != "none" ]; then + echo "Downloading minikube..." + if [ "${MINIKUBE_VERSION}" = "latest" ] || [ "${MINIKUBE_VERSION}" = "lts" ] || [ "${MINIKUBE_VERSION}" = "current" ] || [ "${MINIKUBE_VERSION}" = "stable" ]; then + MINIKUBE_VERSION="latest" + else + find_version_from_git_tags MINIKUBE_VERSION https://github.com/kubernetes/minikube + if [ "${MINIKUBE_VERSION::1}" != "v" ]; then + MINIKUBE_VERSION="v${MINIKUBE_VERSION}" + fi + fi + # latest is also valid in the download URLs + curl -sSL -o /usr/local/bin/minikube "https://storage.googleapis.com/minikube/releases/${MINIKUBE_VERSION}/minikube-linux-${architecture}" + chmod 0755 /usr/local/bin/minikube + if [ "$MINIKUBE_SHA256" = "automatic" ]; then + MINIKUBE_SHA256="$(curl -sSL "https://storage.googleapis.com/minikube/releases/${MINIKUBE_VERSION}/minikube-linux-${architecture}.sha256")" + fi + ([ "${MINIKUBE_SHA256}" = "dev-mode" ] || (echo "${MINIKUBE_SHA256} */usr/local/bin/minikube" | sha256sum -c -)) + if ! type minikube > /dev/null 2>&1; then + echo '(!) minikube installation failed!' + exit 1 + fi + # Create minkube folder with correct privs in case a volume is mounted here + mkdir -p "${USERHOME}/.minikube" + chown -R $USERNAME "${USERHOME}/.minikube" + chmod -R u+wrx "${USERHOME}/.minikube" +fi + +if ! type docker > /dev/null 2>&1; then + echo -e '\n(*) Warning: The docker command was not found.\n\nYou can use one of the following scripts to install it:\n\nhttps://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker-in-docker.md\n\nor\n\nhttps://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker.md' +fi + +echo -e "\nDone!" \ No newline at end of file diff --git a/collection/maven/feature.json b/collection/maven/feature.json new file mode 100644 index 0000000..bc4f5f3 --- /dev/null +++ b/collection/maven/feature.json @@ -0,0 +1,22 @@ +{ + "id": "maven", + "name": "Maven (via SDKMAN!)", + "documentationURL": "https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/maven.md", + "options": { + "version": { + "type": "string", + "proposals": ["latest", "3.8", "3.6", "3.5"], + "default": "latest", + "description": "Select or enter a Maven version to install" + } + }, + "extensions": ["vscjava.vscode-java-pack"], + "containerEnv": { + "SDKMAN_DIR": "/usr/local/sdkman", + "PATH": "${SDKMAN_DIR}/bin:${SDKMAN_DIR}/candidates/maven/current/bin:${PATH}" + }, + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/maven/install.sh b/collection/maven/install.sh new file mode 100644 index 0000000..f8369f5 --- /dev/null +++ b/collection/maven/install.sh @@ -0,0 +1,133 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/maven.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./maven-debian.sh [maven version] [SDKMAN_DIR] [non-root user] [Update rc files flag] + +MAVEN_VERSION=${1:-"latest"} +export SDKMAN_DIR=${2:-"/usr/local/sdkman"} +USERNAME=${3:-"automatic"} +UPDATE_RC=${4:-"true"} + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +updaterc() { + if [ "${UPDATE_RC}" = "true" ]; then + echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." + if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/bash.bashrc + fi + if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/zsh/zshrc + fi + fi +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Use SDKMAN to install something using a partial version match +sdk_install() { + local install_type=$1 + local requested_version=$2 + local prefix=$3 + local suffix="${4:-"\\s*"}" + local full_version_check=${5:-".*-[a-z]+"} + if [ "${requested_version}" = "none" ]; then return; fi + # Blank will install latest stable version + if [ "${requested_version}" = "lts" ] || [ "${requested_version}" = "default" ]; then + requested_version="" + elif echo "${requested_version}" | grep -oE "${full_version_check}" > /dev/null 2>&1; then + echo "${requested_version}" + else + local regex="${prefix}\\K[0-9]+\\.[0-9]+\\.[0-9]+${suffix}" + local version_list="$(. ${SDKMAN_DIR}/bin/sdkman-init.sh && sdk list ${install_type} 2>&1 | grep -oP "${regex}" | tr -d ' ' | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ]; then + requested_version="$(echo "${version_list}" | head -n 1)" + else + set +e + requested_version="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + if [ -z "${requested_version}" ] || ! echo "${version_list}" | grep "^${requested_version//./\\.}$" > /dev/null 2>&1; then + echo -e "Version $2 not found. Available versions:\n${version_list}" >&2 + exit 1 + fi + fi + su ${USERNAME} -c "umask 0002 && . ${SDKMAN_DIR}/bin/sdkman-init.sh && sdk install ${install_type} ${requested_version} && sdk flush archives && sdk flush temp" +} + +export DEBIAN_FRONTEND=noninteractive + +# Install dependencies +check_packages curl ca-certificates zip unzip sed + +# Install sdkman if not installed +if [ ! -d "${SDKMAN_DIR}" ]; then + # Create sdkman group, dir, and set sticky bit + if ! cat /etc/group | grep -e "^sdkman:" > /dev/null 2>&1; then + groupadd -r sdkman + fi + usermod -a -G sdkman ${USERNAME} + umask 0002 + # Install SDKMAN + curl -sSL "https://get.sdkman.io?rcupdate=false" | bash + chown -R :sdkman ${SDKMAN_DIR} + find ${SDKMAN_DIR} -type d | xargs -d '\n' chmod g+s + # Add sourcing of sdkman into bashrc/zshrc files (unless disabled) + updaterc "export SDKMAN_DIR=${SDKMAN_DIR}\n. \${SDKMAN_DIR}/bin/sdkman-init.sh" +fi + +# Install Maven +sdk_install maven ${MAVEN_VERSION} '\s\s' '\s\s' '^[0-9]+\.[0-9]+\.[0-9]+$' +updaterc '[ -z "$M2" ] && export M2=$HOME/.m2' + +echo "Done!" \ No newline at end of file diff --git a/collection/node/feature.json b/collection/node/feature.json new file mode 100644 index 0000000..5a8e81d --- /dev/null +++ b/collection/node/feature.json @@ -0,0 +1,27 @@ +{ + "id": "node", + "name": "Node.js (via nvm) and yarn", + "options": { + "version": { + "type": "string", + "proposals": [ "lts", "latest", "18", "16", "14" ], + "default": "lts", + "description": "Select or enter a Node.js version to install" + }, + "nodeGypDependencies": { + "type": "boolean", + "default": true, + "description": "Install dependencies to compile native node modules (node-gyp)?" + } + }, + "extensions": ["dbaeumer.vscode-eslint"], + "containerEnv": { + "NVM_DIR":"/usr/local/share/nvm", + "NVM_SYMLINK_CURRENT": "true", + "PATH": "${NVM_DIR}/current/bin:${PATH}" + }, + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/node/install.sh b/collection/node/install.sh new file mode 100644 index 0000000..c355168 --- /dev/null +++ b/collection/node/install.sh @@ -0,0 +1,169 @@ +#!/bin/bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/node.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./node-debian.sh [directory to install nvm] [node version to install (use "none" to skip)] [non-root user] [Update rc files flag] [install node-gyp deps] + +export NVM_DIR=${1:-"/usr/local/share/nvm"} +export NODE_VERSION=${2:-"lts"} +USERNAME=${3:-"automatic"} +UPDATE_RC=${4:-"true"} +INSTALL_TOOLS_FOR_NODE_GYP="${5:-true}" +export NVM_VERSION="0.38.0" + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +updaterc() { + if [ "${UPDATE_RC}" = "true" ]; then + echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." + if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/bash.bashrc + fi + if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/zsh/zshrc + fi + fi +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Install dependencies +check_packages apt-transport-https curl ca-certificates tar gnupg2 dirmngr + +# Install yarn +if type yarn > /dev/null 2>&1; then + echo "Yarn already installed." +else + # Import key safely (new method rather than deprecated apt-key approach) and install + curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor > /usr/share/keyrings/yarn-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/yarn-archive-keyring.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list + apt-get update + apt-get -y install --no-install-recommends yarn +fi + +# Adjust node version if required +if [ "${NODE_VERSION}" = "none" ]; then + export NODE_VERSION= +elif [ "${NODE_VERSION}" = "lts" ]; then + export NODE_VERSION="lts/*" +fi + +# Create a symlink to the installed version for use in Dockerfile PATH statements +export NVM_SYMLINK_CURRENT=true + +# Install the specified node version if NVM directory already exists, then exit +if [ -d "${NVM_DIR}" ]; then + echo "NVM already installed." + if [ "${NODE_VERSION}" != "" ]; then + su ${USERNAME} -c ". $NVM_DIR/nvm.sh && nvm install ${NODE_VERSION} && nvm clear-cache" + fi + exit 0 +fi + +# Create nvm group, nvm dir, and set sticky bit +if ! cat /etc/group | grep -e "^nvm:" > /dev/null 2>&1; then + groupadd -r nvm +fi +umask 0002 +usermod -a -G nvm ${USERNAME} +mkdir -p ${NVM_DIR} +chown :nvm ${NVM_DIR} +chmod g+s ${NVM_DIR} +su ${USERNAME} -c "$(cat << EOF + set -e + umask 0002 + # Do not update profile - we'll do this manually + export PROFILE=/dev/null + curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh | bash + source ${NVM_DIR}/nvm.sh + if [ "${NODE_VERSION}" != "" ]; then + nvm alias default ${NODE_VERSION} + fi + nvm clear-cache +EOF +)" 2>&1 +# Update rc files +if [ "${UPDATE_RC}" = "true" ]; then +updaterc "$(cat < /dev/null 2>&1; then + to_install="${to_install} make" + fi + if ! type gcc > /dev/null 2>&1; then + to_install="${to_install} gcc" + fi + if ! type g++ > /dev/null 2>&1; then + to_install="${to_install} g++" + fi + if ! type python3 > /dev/null 2>&1; then + to_install="${to_install} python3-minimal" + fi + if [ ! -z "${to_install}" ]; then + apt_get_update_if_needed + apt-get -y install ${to_install} + fi +fi + +echo "Done!" diff --git a/collection/powershell/feature.json b/collection/powershell/feature.json new file mode 100644 index 0000000..d81bb4b --- /dev/null +++ b/collection/powershell/feature.json @@ -0,0 +1,16 @@ +{ + "id": "powershell", + "name": "PowerShell", + "options": { + "version": { + "type": "string", + "proposals": ["latest", "7.1"], + "default": "latest", + "description": "Select or enter a version of PowerShell." + } + }, + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/powershell/install.sh b/collection/powershell/install.sh new file mode 100644 index 0000000..3110356 --- /dev/null +++ b/collection/powershell/install.sh @@ -0,0 +1,170 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/powershell.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./powershell-debian.sh [version] + +set -e + +POWERSHELL_VERSION=${1:-"latest"} +MICROSOFT_GPG_KEYS_URI="https://packages.microsoft.com/keys/microsoft.asc" +POWERSHELL_ARCHIVE_ARCHITECTURES="amd64" +POWERSHELL_ARCHIVE_VERSION_CODENAMES="stretch buster bionic focal" +GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com:80 +keyserver hkps://keys.openpgp.org +keyserver hkp://keyserver.pgp.com" + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Figure out correct version of a three part version number is not passed +find_version_from_git_tags() { + local variable_name=$1 + local requested_version=${!variable_name} + if [ "${requested_version}" = "none" ]; then return; fi + local repository=$2 + local prefix=${3:-"tags/v"} + local separator=${4:-"."} + local last_part_optional=${5:-"false"} + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local escaped_separator=${separator//./\\.} + local last_part + if [ "${last_part_optional}" = "true" ]; then + last_part="(${escaped_separator}[0-9]+)?" + else + last_part="${escaped_separator}[0-9]+" + fi + local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" + local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" + else + set +e + declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + fi + if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then + echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 + exit 1 + fi + echo "${variable_name}=${!variable_name}" +} + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +install_using_apt() { + # Install dependencies + check_packages apt-transport-https curl ca-certificates gnupg2 dirmngr + # Import key safely (new 'signed-by' method rather than deprecated apt-key approach) and install + get_common_setting MICROSOFT_GPG_KEYS_URI + curl -sSL ${MICROSOFT_GPG_KEYS_URI} | gpg --dearmor > /usr/share/keyrings/microsoft-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/repos/microsoft-${ID}-${VERSION_CODENAME}-prod ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/microsoft.list + + # Update lists + apt-get update -yq + + # Soft version matching for CLI + if [ "${POWERSHELL_VERSION}" = "latest" ] || [ "${POWERSHELL_VERSION}" = "lts" ] || [ "${POWERSHELL_VERSION}" = "stable" ]; then + # Empty, meaning grab whatever "latest" is in apt repo + version_suffix="" + else + version_suffix="=$(apt-cache madison powershell | awk -F"|" '{print $2}' | sed -e 's/^[ \t]*//' | grep -E -m 1 "^(${POWERSHELL_VERSION})(\.|$|\+.*|-.*)")" + + if [ -z ${version_suffix} ] || [ ${version_suffix} = "=" ]; then + echo "Provided POWERSHELL_VERSION (${POWERSHELL_VERSION}) was not found in the apt-cache for this package+distribution combo"; + return 1 + fi + echo "version_suffix ${version_suffix}" + fi + + apt-get install -yq powershell${version_suffix} || return 1 +} + +install_using_github() { + # Fall back on direct download if no apt package exists in microsoft pool + check_packages curl ca-certificates gnupg2 dirmngr libc6 libgcc1 libgssapi-krb5-2 liblttng-ust0 libstdc++6 libunwind8 libuuid1 zlib1g libicu[0-9][0-9] + if ! type git > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get install -y --no-install-recommends git + fi + if [ "${architecture}" = "amd64" ]; then + architecture="x64" + fi + find_version_from_git_tags POWERSHELL_VERSION https://github.com/PowerShell/PowerShell + powershell_filename="powershell-${POWERSHELL_VERSION}-linux-${architecture}.tar.gz" + powershell_target_path="/opt/microsoft/powershell/$(echo ${POWERSHELL_VERSION} | grep -oE '[^\.]+' | head -n 1)" + mkdir -p /tmp/pwsh "${powershell_target_path}" + cd /tmp/pwsh + curl -sSL -o "${powershell_filename}" "https://github.com/PowerShell/PowerShell/releases/download/v${POWERSHELL_VERSION}/${powershell_filename}" + # Ugly - but only way to get sha256 is to parse release HTML. Remove newlines and tags, then look for filename followed by 64 hex characters. + curl -sSL -o "release.html" "https://github.com/PowerShell/PowerShell/releases/tag/v${POWERSHELL_VERSION}" + powershell_archive_sha256="$(cat release.html | tr '\n' ' ' | sed 's|<[^>]*>||g' | grep -oP "${powershell_filename}\s+\K[0-9a-fA-F]{64}" || echo '')" + if [ -z "${powershell_archive_sha256}" ]; then + echo "(!) WARNING: Failed to retrieve SHA256 for archive. Skipping validaiton." + else + echo "SHA256: ${powershell_archive_sha256}" + echo "${powershell_archive_sha256} *${powershell_filename}" | sha256sum -c - + fi + tar xf "${powershell_filename}" -C "${powershell_target_path}" + ln -s "${powershell_target_path}/pwsh" /usr/local/bin/pwsh + rm -rf /tmp/pwsh +} + +export DEBIAN_FRONTEND=noninteractive + +# Source /etc/os-release to get OS info +. /etc/os-release +architecture="$(dpkg --print-architecture)" + +if [[ "${POWERSHELL_ARCHIVE_ARCHITECTURES}" = *"${architecture}"* ]] && [[ "${POWERSHELL_ARCHIVE_VERSION_CODENAMES}" = *"${VERSION_CODENAME}"* ]]; then + install_using_apt || use_github="true" +else + use_github="true" +fi + +if [ "${use_github}" = "true" ]; then + echo "Attempting install from GitHub release..." + install_using_github +fi + +echo "Done!" \ No newline at end of file diff --git a/collection/python/feature.json b/collection/python/feature.json new file mode 100644 index 0000000..18728f3 --- /dev/null +++ b/collection/python/feature.json @@ -0,0 +1,56 @@ +{ + "id": "python", + "name": "Python (may require compilation)", + "description": "Python (may require compilation)", + "options": { + "version": { + "type": "string", + "enum": ["latest", "os-provided", "3.10", "3.9", "3.8", "3.7", "3.6"], + "default": "os-provided", + "description": "Select a Python version to install." + }, + "installTools": { + "type": "boolean", + "default": true, + "description": "Install common Python tools like pylint" + }, + "optimize": { + "type": "boolean", + "default": false, + "description": "Optimize Python for performance when compiled (slow)" + }, + "install_path": { + "type": "string", + "default": "/usr/local/python", + "description": "The path where python will be installed." + }, + "setup_links": { + "type": "boolean", + "default": "true", + "description": "If links and aliases should be created." + } + }, + "containerEnv": { + "PYTHON_PATH": "/usr/local/python", + "PIPX_HOME": "/usr/local/py-utils", + "PIPX_BIN_DIR": "/usr/local/py-utils/bin", + "PATH": "${PYTHON_PATH}/bin:${PATH}:${PIPX_BIN_DIR}" + }, + "extensions": ["ms-python.python", "ms-python.vscode-pylance"], + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python", + "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", + "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", + "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", + "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", + "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", + "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", + "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" + }, + "install": { + "app": "", + "file": "install.sh" + } +} diff --git a/collection/python/python-debian.sh b/collection/python/python-debian.sh new file mode 100644 index 0000000..3a2d429 --- /dev/null +++ b/collection/python/python-debian.sh @@ -0,0 +1,376 @@ +#!/usr/bin/env bash + +PYTHON_VERSION=($PYTHON_VERSION) # 'system' checks the base image first, else installs 'latest' +PYTHON_INSTALL_PATH=($PYTHON_INSTALL_PATH) +PYTHON_SETUP_LINKS=($PYTHON_SETUP_LINKS) + +#PYTHON_VERSION=${1:-"latest"} # 'system' checks the base image first, else installs 'latest' +#PYTHON_INSTALL_PATH=${2:-"/usr/local/python"} +export PIPX_HOME=${3:-"/usr/local/py-utils"} +USERNAME=${4:-"automatic"} +UPDATE_RC=${5:-"true"} +INSTALL_PYTHON_TOOLS=${6:-"true"} +USE_ORYX_IF_AVAILABLE=${7:-"true"} +OPTIMIZE_BUILD_FROM_SOURCE=${8-"false"} + +DEFAULT_UTILS=("pylint" "flake8" "autopep8" "black" "yapf" "mypy" "pydocstyle" "pycodestyle" "bandit" "pipenv" "virtualenv") +PYTHON_SOURCE_GPG_KEYS="64E628F8D684696D B26995E310250568 2D347EA6AA65421D FB9921286F5E1540 3A5CA953F73C700D 04C367C218ADD4FF 0EDDC5F26A45C816 6AF053F07D9DC8D2 C9BE28DEE6DF025C 126EB563A74B06BF D9866941EA5BBD71 ED9D77D5" +GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com:80 +keyserver hkps://keys.openpgp.org +keyserver hkp://keyserver.pgp.com" + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +updaterc() { + if [ "${UPDATE_RC}" = "true" ]; then + echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." + if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/bash.bashrc + fi + if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/zsh/zshrc + fi + fi +} + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Import the specified key in a variable name passed in as +receive_gpg_keys() { + get_common_setting $1 + local keys=${!1} + get_common_setting GPG_KEY_SERVERS true + local keyring_args="" + if [ ! -z "$2" ]; then + mkdir -p "$(dirname \"$2\")" + keyring_args="--no-default-keyring --keyring $2" + fi + + # Use a temporary locaiton for gpg keys to avoid polluting image + export GNUPGHOME="/tmp/tmp-gnupg" + mkdir -p ${GNUPGHOME} + chmod 700 ${GNUPGHOME} + echo -e "disable-ipv6\n${GPG_KEY_SERVERS}" > ${GNUPGHOME}/dirmngr.conf + # GPG key download sometimes fails for some reason and retrying fixes it. + local retry_count=0 + local gpg_ok="false" + set +e + until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; + do + echo "(*) Downloading GPG key..." + ( echo "${keys}" | xargs -n 1 gpg -q ${keyring_args} --recv-keys) 2>&1 && gpg_ok="true" + if [ "${gpg_ok}" != "true" ]; then + echo "(*) Failed getting key, retring in 10s..." + (( retry_count++ )) + sleep 10s + fi + done + set -e + if [ "${gpg_ok}" = "false" ]; then + echo "(!) Failed to get gpg key." + exit 1 + fi +} + +# Figure out correct version of a three part version number is not passed +find_version_from_git_tags() { + local variable_name=$1 + local requested_version=${!variable_name} + if [ "${requested_version}" = "none" ]; then return; fi + local repository=$2 + local prefix=${3:-"tags/v"} + local separator=${4:-"."} + local last_part_optional=${5:-"false"} + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local escaped_separator=${separator//./\\.} + local last_part + if [ "${last_part_optional}" = "true" ]; then + last_part="(${escaped_separator}[0-9]+)?" + else + last_part="${escaped_separator}[0-9]+" + fi + local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" + local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" + else + set +e + declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + fi + if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then + echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 + exit 1 + fi + echo "${variable_name}=${!variable_name}" +} + +# Use Oryx to install something using a partial version match +oryx_install() { + local platform=$1 + local requested_version=$2 + local target_folder=${3:-none} + local ldconfig_folder=${4:-none} + echo "(*) Installing ${platform} ${requested_version} using Oryx..." + check_packages jq + # Soft match if full version not specified + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local version_list="$(oryx platforms --json | jq -r ".[] | select(.Name == \"${platform}\") | .Versions | sort | reverse | @tsv" | tr '\t' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$')" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + requested_version="$(echo "${version_list}" | head -n 1)" + else + set +e + requested_version="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + if [ -z "${requested_version}" ] || ! echo "${version_list}" | grep "^${requested_version//./\\.}$" > /dev/null 2>&1; then + echo -e "(!) Oryx does not support ${platform} version $2\nValid values:\n${version_list}" >&2 + return 1 + fi + echo "(*) Using ${requested_version} in place of $2." + fi + + export ORYX_ENV_TYPE=vsonline-present ORYX_PREFER_USER_INSTALLED_SDKS=true ENABLE_DYNAMIC_INSTALL=true DYNAMIC_INSTALL_ROOT_DIR=/opt + oryx prep --skip-detection --platforms-and-versions "${platform}=${requested_version}" + local opt_folder="/opt/${platform}/${requested_version}" + if [ "${target_folder}" != "none" ] && [ "${target_folder}" != "${opt_folder}" ]; then + ln -s "${opt_folder}" "${target_folder}" + fi + # Update library path add to conf + if [ "${ldconfig_folder}" != "none" ]; then + echo "/opt/${platform}/${requested_version}/lib" >> "/etc/ld.so.conf.d/${platform}.conf" + ldconfig + fi +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +install_from_source() { + if [ -d "${PYTHON_INSTALL_PATH}" ]; then + echo "(!) Path ${PYTHON_INSTALL_PATH} already exists. Remove this existing path or select a different one." + exit 1 + fi + echo "(*) Building Python ${PYTHON_VERSION} from source..." + # Install prereqs if missing + check_packages curl ca-certificates gnupg2 tar make gcc libssl-dev zlib1g-dev libncurses5-dev \ + libbz2-dev libreadline-dev libxml2-dev xz-utils libgdbm-dev tk-dev dirmngr \ + libxmlsec1-dev libsqlite3-dev libffi-dev liblzma-dev uuid-dev + if ! type git > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends git + fi + + # Find version using soft match + find_version_from_git_tags PYTHON_VERSION "https://github.com/python/cpython" + + # Download tgz of source + mkdir -p /tmp/python-src "${PYTHON_INSTALL_PATH}" + cd /tmp/python-src + local tgz_filename="Python-${PYTHON_VERSION}.tgz" + local tgz_url="https://www.python.org/ftp/python/${PYTHON_VERSION}/${tgz_filename}" + echo "Downloading ${tgz_filename}..." + curl -sSL -o "/tmp/python-src/${tgz_filename}" "${tgz_url}" + + # Verify signature + receive_gpg_keys PYTHON_SOURCE_GPG_KEYS + echo "Downloading ${tgz_filename}.asc..." + curl -sSL -o "/tmp/python-src/${tgz_filename}.asc" "${tgz_url}.asc" + gpg --verify "${tgz_filename}.asc" + + # Update min protocol for testing only - https://bugs.python.org/issue41561 + cp /etc/ssl/openssl.cnf /tmp/python-src/ + sed -i -E 's/MinProtocol[=\ ]+.*/MinProtocol = TLSv1.0/g' /tmp/python-src/openssl.cnf + export OPENSSL_CONF=/tmp/python-src/openssl.cnf + + # Untar and build + tar -xzf "/tmp/python-src/${tgz_filename}" -C "/tmp/python-src" --strip-components=1 + local config_args="" + if [ "${OPTIMIZE_BUILD_FROM_SOURCE}" = "true" ]; then + config_args="--enable-optimizations" + fi + ./configure --prefix="${PYTHON_INSTALL_PATH}" --with-ensurepip=install ${config_args} + make -j 8 + make install + cd /tmp + rm -rf /tmp/python-src ${GNUPGHOME} /tmp/vscdc-settings.env + chown -R ${USERNAME} "${PYTHON_INSTALL_PATH}" + ln -s ${PYTHON_INSTALL_PATH}/bin/python3 ${PYTHON_INSTALL_PATH}/bin/python + ln -s ${PYTHON_INSTALL_PATH}/bin/pip3 ${PYTHON_INSTALL_PATH}/bin/pip + ln -s ${PYTHON_INSTALL_PATH}/bin/idle3 ${PYTHON_INSTALL_PATH}/bin/idle + ln -s ${PYTHON_INSTALL_PATH}/bin/pydoc3 ${PYTHON_INSTALL_PATH}/bin/pydoc + ln -s ${PYTHON_INSTALL_PATH}/bin/python3-config ${PYTHON_INSTALL_PATH}/bin/python-config +} + +install_using_oryx() { + if [ -d "${PYTHON_INSTALL_PATH}" ]; then + echo "(!) Path ${PYTHON_INSTALL_PATH} already exists. Remove this existing path or select a different one." + exit 1 + fi + oryx_install "python" "${PYTHON_VERSION}" "${PYTHON_INSTALL_PATH}" "lib" || return 1 + ln -s ${PYTHON_INSTALL_PATH}/bin/idle3 ${PYTHON_INSTALL_PATH}/bin/idle + ln -s ${PYTHON_INSTALL_PATH}/bin/pydoc3 ${PYTHON_INSTALL_PATH}/bin/pydoc + ln -s ${PYTHON_INSTALL_PATH}/bin/python3-config ${PYTHON_INSTALL_PATH}/bin/python-config +} + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# General requirements +check_packages curl ca-certificates gnupg2 tar make gcc libssl-dev zlib1g-dev libncurses5-dev \ + libbz2-dev libreadline-dev libxml2-dev xz-utils libgdbm-dev tk-dev dirmngr \ + libxmlsec1-dev libsqlite3-dev libffi-dev liblzma-dev uuid-dev + + +# Install python from source if needed +if [ "${PYTHON_VERSION}" != "none" ]; then + # If the os-provided versions are "good enough", detect that and bail out. + if [ ${PYTHON_VERSION} = "os-provided" ] || [ ${PYTHON_VERSION} = "system" ]; then + check_packages python3 python3-doc python3-pip python3-venv python3-dev python3-tk + PYTHON_INSTALL_PATH="/usr" + should_install_from_source=false + elif [ "$(dpkg --print-architecture)" = "amd64" ] && [ "${USE_ORYX_IF_AVAILABLE}" = "true" ] && type oryx > /dev/null 2>&1; then + install_using_oryx || should_install_from_source=true + else + should_install_from_source=true + fi + if [ "${should_install_from_source}" = "true" ]; then + install_from_source + fi + updaterc "if [[ \"\${PATH}\" != *\"${PYTHON_INSTALL_PATH}/bin\"* ]]; then export PATH=${PYTHON_INSTALL_PATH}/bin:\${PATH}; fi" +fi + +# If not installing python tools, exit +if [ "${INSTALL_PYTHON_TOOLS}" != "true" ]; then + echo "Done!" + exit 0 +fi + +export PIPX_BIN_DIR="${PIPX_HOME}/bin" +export PATH="${PYTHON_INSTALL_PATH}/bin:${PIPX_BIN_DIR}:${PATH}" + +# Create pipx group, dir, and set sticky bit +if ! cat /etc/group | grep -e "^pipx:" > /dev/null 2>&1; then + groupadd -r pipx +fi +usermod -a -G pipx ${USERNAME} +umask 0002 +mkdir -p ${PIPX_BIN_DIR} +chown :pipx ${PIPX_HOME} ${PIPX_BIN_DIR} +chmod g+s ${PIPX_HOME} ${PIPX_BIN_DIR} + +# Update pip if not using os provided python +if [ ${PYTHON_VERSION} != "os-provided" ] && [ ${PYTHON_VERSION} != "system" ]; then + echo "Updating pip..." + ${PYTHON_INSTALL_PATH}/bin/python3 -m pip install --no-cache-dir --upgrade pip +fi + +# Install tools +echo "Installing Python tools..." +export PYTHONUSERBASE=/tmp/pip-tmp +export PIP_CACHE_DIR=/tmp/pip-tmp/cache +pipx_path="" +if ! type pipx > /dev/null 2>&1; then + pip3 install --disable-pip-version-check --no-cache-dir --user pipx 2>&1 + /tmp/pip-tmp/bin/pipx install --pip-args=--no-cache-dir pipx + pipx_path="/tmp/pip-tmp/bin/" +fi +for util in ${DEFAULT_UTILS[@]}; do + if ! type ${util} > /dev/null 2>&1; then + ${pipx_path}pipx install --system-site-packages --pip-args '--no-cache-dir --force-reinstall' ${util} + else + echo "${util} already installed. Skipping." + fi +done +rm -rf /tmp/pip-tmp + +echo "(*) Updating RC files." + +updaterc "$(cat << EOF +export PIPX_HOME="${PIPX_HOME}" +export PIPX_BIN_DIR="${PIPX_BIN_DIR}" +if [[ "\${PATH}" != *"\${PIPX_BIN_DIR}"* ]]; then export PATH="\${PATH}:\${PIPX_BIN_DIR}"; fi +EOF +)" + +echo "(*) About to do final Python setup." + +INSTALLED_PYTHON_VERSION=$(${PYTHON_INSTALL_PATH}/bin/python --version 2>&1 | grep -Po '(?<=Python )(.+)') +if [[ ! -z "$INSTALLED_PYTHON_VERSION" ]] ; then + echo "(*) Installed Python version is ${INSTALLED_PYTHON_VERSION}" + PYTHON_MAJOR_VERSION=$(echo ${INSTALLED_PYTHON_VERSION} | cut -f1 -d'.') + PYTHON_MINOR_VERSION=$(echo ${INSTALLED_PYTHON_VERSION} | cut -f2 -d'.') + + if [ "${PYTHON_SETUP_LINKS}" = "true" ]; then + mkdir -p /opt/python + ln -s /usr/share/python /opt/python/${INSTALLED_PYTHON_VERSION} + ln -s /opt/python/${INSTALLED_PYTHON_VERSION} /opt/python/${PYTHON_MAJOR_VERSION}.${PYTHON_MINOR_VERSION} + ln -s /opt/python/${PYTHON_MAJOR_VERSION}.${PYTHON_MINOR_VERSION} /opt/python/${PYTHON_MAJOR_VERSION} + ln -s /opt/python/${INSTALLED_PYTHON_VERSION} /opt/python/latest + ln -s /opt/python/${INSTALLED_PYTHON_VERSION} /opt/python/stable + + mkdir -p /home/${USERNAME}/.python + ln -s /opt/python/${INSTALLED_PYTHON_VERSION} /home/${USERNAME}/.python/current + fi +else + echo "(*) Failed to get installed Python version." +fi + +echo "(*) Done with Python feature." diff --git a/collection/ruby/feature.json b/collection/ruby/feature.json new file mode 100644 index 0000000..3ba0a36 --- /dev/null +++ b/collection/ruby/feature.json @@ -0,0 +1,25 @@ +{ + "id": "ruby", + "name": "Ruby (via rvm)", + "options": { + "version": { + "type": "string", + "proposals": ["latest", "3.1", "3.0", "2.7"], + "default": "latest", + "description": "Select or enter a Ruby version to install" + } + }, + "extensions": [ + "rebornix.Ruby" + ], + "containerEnv": { + "GEM_PATH": "/usr/local/rvm/gems/default:/usr/local/rvm/gems/default@global", + "GEM_HOME": "/usr/local/rvm/gems/default", + "MY_RUBY_HOME": "/usr/local/rvm/rubies/default", + "PATH": "/usr/local/rvm/gems/default/bin:/usr/local/rvm/gems/default@global/bin:/usr/local/rvm/rubies/default/bin:${PATH}" + }, + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/ruby/install.sh b/collection/ruby/install.sh new file mode 100644 index 0000000..9908510 --- /dev/null +++ b/collection/ruby/install.sh @@ -0,0 +1,271 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/ruby.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./ruby-debian.sh [Ruby version] [non-root user] [Add to rc files flag] [Install tools flag] + +RUBY_VERSION=${1:-"latest"} +USERNAME=${2:-"automatic"} +UPDATE_RC=${3:-"true"} +INSTALL_RUBY_TOOLS=${6:-"true"} + +# Note: ruby-debug-ide will install the right version of debase if missing and +# installing debase directly fails on Ruby 3.1.0 as of 1/7/2022, so omitting. +DEFAULT_GEMS="rake ruby-debug-ide" + +RVM_GPG_KEYS="409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB" +GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com:80 +keyserver hkps://keys.openpgp.org +keyserver hkp://keyserver.pgp.com" + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +updaterc() { + if [ "${UPDATE_RC}" = "true" ]; then + echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." + if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/bash.bashrc + fi + if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/zsh/zshrc + fi + fi +} + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Import the specified key in a variable name passed in as +receive_gpg_keys() { + get_common_setting $1 + local keys=${!1} + get_common_setting GPG_KEY_SERVERS true + local keyring_args="" + if [ ! -z "$2" ]; then + keyring_args="--no-default-keyring --keyring \"$2\"" + fi + + # Use a temporary locaiton for gpg keys to avoid polluting image + export GNUPGHOME="/tmp/tmp-gnupg" + mkdir -p ${GNUPGHOME} + chmod 700 ${GNUPGHOME} + echo -e "disable-ipv6\n${GPG_KEY_SERVERS}" > ${GNUPGHOME}/dirmngr.conf + # GPG key download sometimes fails for some reason and retrying fixes it. + local retry_count=0 + local gpg_ok="false" + set +e + until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; + do + echo "(*) Downloading GPG key..." + ( echo "${keys}" | xargs -n 1 gpg -q ${keyring_args} --recv-keys) 2>&1 && gpg_ok="true" + if [ "${gpg_ok}" != "true" ]; then + echo "(*) Failed getting key, retring in 10s..." + (( retry_count++ )) + sleep 10s + fi + done + set -e + if [ "${gpg_ok}" = "false" ]; then + echo "(!) Failed to get gpg key." + exit 1 + fi +} + +# Figure out correct version of a three part version number is not passed +find_version_from_git_tags() { + local variable_name=$1 + local requested_version=${!variable_name} + if [ "${requested_version}" = "none" ]; then return; fi + local repository=$2 + local prefix=${3:-"tags/v"} + local separator=${4:-"."} + local last_part_optional=${5:-"false"} + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local escaped_separator=${separator//./\\.} + local last_part + if [ "${last_part_optional}" = "true" ]; then + last_part="(${escaped_separator}[0-9]+)?" + else + last_part="${escaped_separator}[0-9]+" + fi + local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" + local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" + else + set +e + declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + fi + if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then + echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 + exit 1 + fi + echo "${variable_name}=${!variable_name}" +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +architecture="$(uname -m)" +if [ "${architecture}" != "amd64" ] && [ "${architecture}" != "x86_64" ] && [ "${architecture}" != "arm64" ] && [ "${architecture}" != "aarch64" ]; then + echo "(!) Architecture $architecture unsupported" + exit 1 +fi + +# Install dependencies +check_packages curl ca-certificates software-properties-common build-essential gnupg2 libreadline-dev \ + procps dirmngr gawk autoconf automake bison libffi-dev libgdbm-dev libncurses5-dev \ + libsqlite3-dev libtool libyaml-dev pkg-config sqlite3 zlib1g-dev libgmp-dev libssl-dev +if ! type git > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends git +fi + + +# Figure out correct version of a three part version number is not passed +find_version_from_git_tags RUBY_VERSION "https://github.com/ruby/ruby" "tags/v" "_" + +# Just install Ruby if RVM already installed +if [ -d "/usr/local/rvm" ]; then + echo "Ruby Version Manager already exists." + if [ "${RUBY_VERSION}" != "none" ]; then + echo "Installing specified Ruby version." + su ${USERNAME} -c "&& rvm install ruby ${RUBY_VERSION}" + fi + SKIP_GEM_INSTALL="false" +else + # Install RVM + receive_gpg_keys RVM_GPG_KEYS + # Determine appropriate settings for rvm installer + if [ "${RUBY_VERSION}" = "none" ]; then + RVM_INSTALL_ARGS="" + else + if [ "${RUBY_VERSION}" = "latest" ] || [ "${RUBY_VERSION}" = "current" ] || [ "${RUBY_VERSION}" = "lts" ]; then + RVM_INSTALL_ARGS="--ruby" + RUBY_VERSION="" + else + RVM_INSTALL_ARGS="--ruby=${RUBY_VERSION}" + fi + if [ "${INSTALL_RUBY_TOOLS}" = "true" ]; then + SKIP_GEM_INSTALL="true" + else + DEFAULT_GEMS="" + fi + fi + # Create rvm group as a system group to reduce the odds of conflict with local user UIDs + if ! cat /etc/group | grep -e "^rvm:" > /dev/null 2>&1; then + groupadd -r rvm + fi + # Install rvm + curl -sSL https://get.rvm.io | bash -s stable --ignore-dotfiles ${RVM_INSTALL_ARGS} --with-default-gems="${DEFAULT_GEMS}" 2>&1 + usermod -aG rvm ${USERNAME} + su ${USERNAME} -c ". /usr/local/rvm/scripts/rvm && rvm fix-permissions system" + rm -rf ${GNUPGHOME} +fi + +if [ "${INSTALL_RUBY_TOOLS}" = "true" ]; then + # Non-root user may not have "gem" in path when script is run and no ruby version + # is installed by rvm, so handle this by using root's default gem in this case + ROOT_GEM='$(which gem || echo "")' + su ${USERNAME} -c ". /usr/local/rvm/scripts/rvm && \"$(which gem || echo ${ROOT_GEM})\" install ${DEFAULT_GEMS}" +fi + +# VS Code server usually first in the path, so silence annoying rvm warning (that does not apply) and then source it +updaterc "if ! grep rvm_silence_path_mismatch_check_flag \$HOME/.rvmrc > /dev/null 2>&1; then echo 'rvm_silence_path_mismatch_check_flag=1' >> \$HOME/.rvmrc; fi\nsource /usr/local/rvm/scripts/rvm > /dev/null 2>&1" + +# Install rbenv/ruby-build for good measure +git clone --depth=1 \ + -c core.eol=lf \ + -c core.autocrlf=false \ + -c fsck.zeroPaddedFilemode=ignore \ + -c fetch.fsck.zeroPaddedFilemode=ignore \ + -c receive.fsck.zeroPaddedFilemode=ignore \ + https://github.com/rbenv/rbenv.git /usr/local/share/rbenv +ln -s /usr/local/share/rbenv/bin/rbenv /usr/local/bin +updaterc 'eval "$(rbenv init -)"' +git clone --depth=1 \ + -c core.eol=lf \ + -c core.autocrlf=false \ + -c fsck.zeroPaddedFilemode=ignore \ + -c fetch.fsck.zeroPaddedFilemode=ignore \ + -c receive.fsck.zeroPaddedFilemode=ignore \ + https://github.com/rbenv/ruby-build.git /usr/local/share/ruby-build +mkdir -p /root/.rbenv/plugins +ln -s /usr/local/share/ruby-build /root/.rbenv/plugins/ruby-build +if [ "${USERNAME}" != "root" ]; then + mkdir -p /home/${USERNAME}/.rbenv/plugins + chown -R ${USERNAME} /home/${USERNAME}/.rbenv + ln -s /usr/local/share/ruby-build /home/${USERNAME}/.rbenv/plugins/ruby-build +fi + +# Clean up +source /usr/local/rvm/scripts/rvm +rvm cleanup all +gem cleanup +echo "Done!" diff --git a/collection/rust/feature.json b/collection/rust/feature.json new file mode 100644 index 0000000..47df7e1 --- /dev/null +++ b/collection/rust/feature.json @@ -0,0 +1,43 @@ +{ + "id": "rust", + "name": "Rust", + "options": { + "version": { + "type": "string", + "proposals": ["latest", "1.55", "1.54", "1.53"], + "default": "latest", + "description": "Select or enter a version of Rust to install." + }, + "profile": { + "type": "string", + "proposals": ["minimal", "default", "complete"], + "default": "minimal", + "description": "Select a rustup install profile." + } + }, + "extensions": [ + "vadimcn.vscode-lldb", + "mutantdino.resourcemonitor", + "matklad.rust-analyzer", + "tamasfe.even-better-toml", + "serayuzgur.crates" + ], + "containerEnv": { + "CARGO_HOME": "/usr/local/cargo", + "RUSTUP_HOME": "/usr/local/rustup", + "PATH": "${CARGO_HOME}/bin:${PATH}" + }, + "capAdd": [ "SYS_PTRACE" ], + "securityOpt": ["seccomp=unconfined"], + "settings": { + "lldb.executable": "/usr/bin/lldb", + "files.watcherExclude": { + "**/target/**": true + }, + "rust-analyzer.checkOnSave.command": "clippy" + }, + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/rust/install.sh b/collection/rust/install.sh new file mode 100644 index 0000000..1d90b14 --- /dev/null +++ b/collection/rust/install.sh @@ -0,0 +1,199 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/rust.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./rust-debian.sh [CARGO_HOME] [RUSTUP_HOME] [non-root user] [add CARGO/RUSTUP_HOME to rc files flag] [whether to update rust] [Rust version] [rustup install profile] + +export CARGO_HOME=${1:-"/usr/local/cargo"} +export RUSTUP_HOME=${2:-"/usr/local/rustup"} +USERNAME=${3:-"automatic"} +UPDATE_RC=${4:-"true"} +UPDATE_RUST=${5:-"false"} +RUST_VERSION=${6:-"latest"} +RUSTUP_PROFILE=${7:-"minimal"} + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Figure out correct version of a three part version number is not passed +find_version_from_git_tags() { + local variable_name=$1 + local requested_version=${!variable_name} + if [ "${requested_version}" = "none" ]; then return; fi + local repository=$2 + local prefix=${3:-"tags/v"} + local separator=${4:-"."} + local last_part_optional=${5:-"false"} + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local escaped_separator=${separator//./\\.} + local last_part + if [ "${last_part_optional}" = "true" ]; then + last_part="(${escaped_separator}[0-9]+)?" + else + last_part="${escaped_separator}[0-9]+" + fi + local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" + local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" + else + set +e + declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + fi + if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then + echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 + exit 1 + fi + echo "${variable_name}=${!variable_name}" +} + +updaterc() { + if [ "${UPDATE_RC}" = "true" ]; then + echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." + if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/bash.bashrc + fi + if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/zsh/zshrc + fi + fi +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +export DEBIAN_FRONTEND=noninteractive + +# Install curl, lldb, python3-minimal,libpython and rust dependencies if missing +if ! dpkg -s curl ca-certificates gnupg2 lldb python3-minimal gcc libc6-dev > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends curl ca-certificates gcc libc6-dev + apt-get -y install lldb python3-minimal libpython3.? +fi + +architecture="$(dpkg --print-architecture)" +download_architecture="${architecture}" +case ${download_architecture} in + amd64) + download_architecture="x86_64" + ;; + arm64) + download_architecture="aarch64" + ;; + *) echo "(!) Architecture ${architecture} not supported." + exit 1 + ;; +esac + +# Install Rust +umask 0002 +if ! cat /etc/group | grep -e "^rustlang:" > /dev/null 2>&1; then + groupadd -r rustlang +fi +usermod -a -G rustlang "${USERNAME}" +mkdir -p "${CARGO_HOME}" "${RUSTUP_HOME}" +chown :rustlang "${RUSTUP_HOME}" "${CARGO_HOME}" +chmod g+r+w+s "${RUSTUP_HOME}" "${CARGO_HOME}" + +if [ "${RUST_VERSION}" = "none" ] || type rustup > /dev/null 2>&1; then + echo "Rust already installed. Skipping..." +else + if [ "${RUST_VERSION}" != "latest" ] && [ "${RUST_VERSION}" != "lts" ] && [ "${RUST_VERSION}" != "stable" ]; then + # Find version using soft match + if ! type git > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends git + fi + find_version_from_git_tags RUST_VERSION "https://github.com/rust-lang/rust" "tags/" + default_toolchain_arg="--default-toolchain ${RUST_VERSION}" + fi + echo "Installing Rust..." + # Download and verify rustup sha + mkdir -p /tmp/rustup/target/${download_architecture}-unknown-linux-gnu/release/ + curl -sSL --proto '=https' --tlsv1.2 "https://static.rust-lang.org/rustup/dist/${download_architecture}-unknown-linux-gnu/rustup-init" -o /tmp/rustup/target/${download_architecture}-unknown-linux-gnu/release/rustup-init + curl -sSL --proto '=https' --tlsv1.2 "https://static.rust-lang.org/rustup/dist/${download_architecture}-unknown-linux-gnu/rustup-init.sha256" -o /tmp/rustup/rustup-init.sha256 + cd /tmp/rustup + sha256sum -c rustup-init.sha256 + chmod +x target/${download_architecture}-unknown-linux-gnu/release/rustup-init + target/${download_architecture}-unknown-linux-gnu/release/rustup-init -y --no-modify-path --profile ${RUSTUP_PROFILE} ${default_toolchain_arg} + cd ~ + rm -rf /tmp/rustup +fi + +export PATH=${CARGO_HOME}/bin:${PATH} +if [ "${UPDATE_RUST}" = "true" ]; then + echo "Updating Rust..." + rustup update 2>&1 +fi +echo "Installing common Rust dependencies..." +rustup component add rls rust-analysis rust-src rustfmt clippy 2>&1 + +# Add CARGO_HOME, RUSTUP_HOME and bin directory into bashrc/zshrc files (unless disabled) +updaterc "$(cat << EOF +export RUSTUP_HOME="${RUSTUP_HOME}" +export CARGO_HOME="${CARGO_HOME}" +if [[ "\${PATH}" != *"\${CARGO_HOME}/bin"* ]]; then export PATH="\${CARGO_HOME}/bin:\${PATH}"; fi +EOF +)" + +# Make files writable for rustlang group +chmod -R g+r+w "${RUSTUP_HOME}" "${CARGO_HOME}" + +echo "Done!" + diff --git a/collection/sshd/feature.json b/collection/sshd/feature.json new file mode 100644 index 0000000..4de71d9 --- /dev/null +++ b/collection/sshd/feature.json @@ -0,0 +1,17 @@ +{ + "id": "sshd", + "name": "SSH server", + "options": { + "version": { + "type": "string", + "enum": ["latest"], + "default": "latest", + "description": "Currently unused." + } + }, + "entrypoint": "/usr/local/share/ssh-init.sh", + "install": { + "app": "", + "file": "install.sh" + } +} \ No newline at end of file diff --git a/collection/sshd/install.sh b/collection/sshd/install.sh new file mode 100644 index 0000000..eb33a09 --- /dev/null +++ b/collection/sshd/install.sh @@ -0,0 +1,169 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/sshd.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./sshd-debian.sh [SSH Port (don't use 22)] [non-root user] [start sshd now flag] [new password for user] [fix environment flag] +# +# Note: You can change your user's password with "sudo passwd $(whoami)" (or just "passwd" if running as root). + +SSHD_PORT=${1:-"2222"} +USERNAME=${2:-"automatic"} +START_SSHD=${3:-"false"} +NEW_PASSWORD=${4:-"skip"} +FIX_ENVIRONMENT=${5:-"true"} + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Install openssh-server openssh-client +check_packages openssh-server openssh-client lsof + +# Generate password if new password set to the word "random" +if [ "${NEW_PASSWORD}" = "random" ]; then + NEW_PASSWORD="$(openssl rand -hex 16)" + EMIT_PASSWORD="true" +elif [ "${NEW_PASSWORD}" != "skip" ]; then + # If new password not set to skip, set it for the specified user + echo "${USERNAME}:${NEW_PASSWORD}" | chpasswd +fi + +# Add user to ssh group +if [ "${USERNAME}" != "root" ]; then + usermod -aG ssh ${USERNAME} +fi + +# Setup sshd +mkdir -p /var/run/sshd +sed -i 's/session\s*required\s*pam_loginuid\.so/session optional pam_loginuid.so/g' /etc/pam.d/sshd +sed -i 's/#*PermitRootLogin prohibit-password/PermitRootLogin yes/g' /etc/ssh/sshd_config +sed -i -E "s/#*\s*Port\s+.+/Port ${SSHD_PORT}/g" /etc/ssh/sshd_config +# Need to UsePAM so /etc/environment is processed +sed -i -E "s/#?\s*UsePAM\s+.+/UsePAM yes/g" /etc/ssh/sshd_config + +# Script to store variables that exist at the time the ENTRYPOINT is fired +store_env_script="$(cat << 'EOF' +# Wire in codespaces secret processing to zsh if present (since may have been added to image after script was run) +if [ -f /etc/zsh/zlogin ] && ! grep '/etc/profile.d/00-restore-secrets.sh' /etc/zsh/zlogin > /dev/null 2>&1; then + echo -e "if [ -f /etc/profile.d/00-restore-secrets.sh ]; then . /etc/profile.d/00-restore-secrets.sh; fi\n$(cat /etc/zsh/zlogin 2>/dev/null || echo '')" | sudoIf tee /etc/zsh/zlogin > /dev/null +fi +EOF +)" + +# Script to ensure login shells get the latest Codespaces secrets +restore_secrets_script="$(cat << 'EOF' +#!/bin/sh +if [ "${CODESPACES}" != "true" ] || [ "${VSCDC_FIXED_SECRETS}" = "true" ] || [ ! -z "${GITHUB_CODESPACES_TOKEN}" ]; then + # Not codespaces, already run, or secrets already in environment, so return + return +fi +if [ -f /workspaces/.codespaces/shared/.env-secrets ]; then + while read line + do + key=$(echo $line | sed "s/=.*//") + value=$(echo $line | sed "s/$key=//1") + decodedValue=$(echo $value | base64 -d) + export $key="$decodedValue" + done < /workspaces/.codespaces/shared/.env-secrets +fi +export VSCDC_FIXED_SECRETS=true +EOF +)" + +# Write out a scripts that can be referenced as an ENTRYPOINT to auto-start sshd and fix login environments +tee /usr/local/share/ssh-init.sh > /dev/null \ +<< 'EOF' +#!/usr/bin/env bash +# This script is intended to be run as root with a container that runs as root (even if you connect with a different user) +# However, it supports running as a user other than root if passwordless sudo is configured for that same user. + +set -e + +sudoIf() +{ + if [ "$(id -u)" -ne 0 ]; then + sudo "$@" + else + "$@" + fi +} + +EOF +if [ "${FIX_ENVIRONMENT}" = "true" ]; then + echo "${store_env_script}" >> /usr/local/share/ssh-init.sh + echo "${restore_secrets_script}" > /etc/profile.d/00-restore-secrets.sh + chmod +x /etc/profile.d/00-restore-secrets.sh + # Wire in zsh if present + if type zsh > /dev/null 2>&1; then + echo -e "if [ -f /etc/profile.d/00-restore-secrets.sh ]; then . /etc/profile.d/00-restore-secrets.sh; fi\n$(cat /etc/zsh/zlogin 2>/dev/null || echo '')" > /etc/zsh/zlogin + fi +fi +tee -a /usr/local/share/ssh-init.sh > /dev/null \ +<< 'EOF' + +# ** Start SSH server ** +sudoIf /etc/init.d/ssh start 2>&1 | sudoIf tee /tmp/sshd.log > /dev/null + +set +e +exec "$@" +EOF +chmod +x /usr/local/share/ssh-init.sh + +# If we should start sshd now, do so +if [ "${START_SSHD}" = "true" ]; then + /usr/local/share/ssh-init.sh +fi + +# Output success details +echo -e "Done!\n\n- Port: ${SSHD_PORT}\n- User: ${USERNAME}" +if [ "${EMIT_PASSWORD}" = "true" ]; then + echo "- Password: ${NEW_PASSWORD}" +fi +echo -e "\nForward port ${SSHD_PORT} to your local machine and run:\n\n ssh -p ${SSHD_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null ${USERNAME}@localhost\n" diff --git a/collection/terraform/feature.json b/collection/terraform/feature.json new file mode 100644 index 0000000..02670a5 --- /dev/null +++ b/collection/terraform/feature.json @@ -0,0 +1,39 @@ +{ + "id": "terraform", + "name": "Terraform, tflint, and TFGrunt", + "options": { + "version": { + "type": "string", + "proposals": ["latest", "1.1", "1.0", "0.15"], + "default": "latest", + "description": "Terraform version" + }, + "tflint": { + "type": "string", + "proposals": ["latest"], + "default": "latest", + "description": "Tflint version" + }, + "terragrunt": { + "type": "string", + "proposals": ["latest"], + "default": "latest", + "description": "Terragrunt version" + } + }, + "extensions": [ + "HashiCorp.terraform", + "ms-azuretools.vscode-azureterraform" + ], + "settings": { + "terraform.languageServer": { + "enabled": true, + "args": [] + }, + "azureTerraform.terminal": "integrated", + "install": { + "app": "", + "file": "install.sh" + } + } +} \ No newline at end of file diff --git a/collection/terraform/install.sh b/collection/terraform/install.sh new file mode 100644 index 0000000..f2614ae --- /dev/null +++ b/collection/terraform/install.sh @@ -0,0 +1,218 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/terraform.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./terraform-debian.sh [terraform version] [tflint version] [terragrunt version] [terraform SHA] [tflint SHA] [terragrunt SHA] + +set -e + +TERRAFORM_VERSION="${1:-"latest"}" +TFLINT_VERSION="${2:-"latest"}" +TERRAGRUNT_VERSION="${3:-"latest"}" +TERRAFORM_SHA256="${4:-"automatic"}" +TFLINT_SHA256="${5:-"automatic"}" +TERRAGRUNT_SHA256="${6:-"automatic"}" + +TERRAFORM_GPG_KEY="72D7468F" +TFLINT_GPG_KEY_URI="https://raw.githubusercontent.com/terraform-linters/tflint/master/8CE69160EB3F2FE9.key" +GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com:80 +keyserver hkps://keys.openpgp.org +keyserver hkp://keyserver.pgp.com" + +architecture="$(uname -m)" +case ${architecture} in + x86_64) architecture="amd64";; + aarch64 | armv8*) architecture="arm64";; + aarch32 | armv7* | armvhf*) architecture="arm";; + i?86) architecture="386";; + *) echo "(!) Architecture ${architecture} unsupported"; exit 1 ;; +esac + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Import the specified key in a variable name passed in as +receive_gpg_keys() { + get_common_setting $1 + local keys=${!1} + get_common_setting GPG_KEY_SERVERS true + local keyring_args="" + if [ ! -z "$2" ]; then + keyring_args="--no-default-keyring --keyring $2" + fi + + # Use a temporary locaiton for gpg keys to avoid polluting image + export GNUPGHOME="/tmp/tmp-gnupg" + mkdir -p ${GNUPGHOME} + chmod 700 ${GNUPGHOME} + echo -e "disable-ipv6\n${GPG_KEY_SERVERS}" > ${GNUPGHOME}/dirmngr.conf + # GPG key download sometimes fails for some reason and retrying fixes it. + local retry_count=0 + local gpg_ok="false" + set +e + until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; + do + echo "(*) Downloading GPG key..." + ( echo "${keys}" | xargs -n 1 gpg -q ${keyring_args} --recv-keys) 2>&1 && gpg_ok="true" + if [ "${gpg_ok}" != "true" ]; then + echo "(*) Failed getting key, retring in 10s..." + (( retry_count++ )) + sleep 10s + fi + done + set -e + if [ "${gpg_ok}" = "false" ]; then + echo "(!) Failed to get gpg key." + exit 1 + fi +} + +# Figure out correct version of a three part version number is not passed +find_version_from_git_tags() { + local variable_name=$1 + local requested_version=${!variable_name} + if [ "${requested_version}" = "none" ]; then return; fi + local repository=$2 + local prefix=${3:-"tags/v"} + local separator=${4:-"."} + local last_part_optional=${5:-"false"} + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local escaped_separator=${separator//./\\.} + local last_part + if [ "${last_part_optional}" = "true" ]; then + last_part="(${escaped_separator}[0-9]+)?" + else + last_part="${escaped_separator}[0-9]+" + fi + local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" + local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" + else + set +e + declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + fi + if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then + echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 + exit 1 + fi + echo "${variable_name}=${!variable_name}" +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Install dependencies if missing +check_packages curl ca-certificates gnupg2 dirmngr coreutils unzip +if ! type git > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends git +fi + +# Verify requested version is available, convert latest +find_version_from_git_tags TERRAFORM_VERSION 'https://github.com/hashicorp/terraform' +find_version_from_git_tags TFLINT_VERSION 'https://github.com/terraform-linters/tflint' +find_version_from_git_tags TERRAGRUNT_VERSION 'https://github.com/gruntwork-io/terragrunt' + +mkdir -p /tmp/tf-downloads +cd /tmp/tf-downloads + +# Install Terraform, tflint, Terragrunt +echo "Downloading terraform..." +terraform_filename="terraform_${TERRAFORM_VERSION}_linux_${architecture}.zip" +curl -sSL -o ${terraform_filename} "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/${terraform_filename}" +if [ "${TERRAFORM_SHA256}" != "dev-mode" ]; then + if [ "${TERRAFORM_SHA256}" = "automatic" ]; then + receive_gpg_keys TERRAFORM_GPG_KEY + curl -sSL -o terraform_SHA256SUMS https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_SHA256SUMS + curl -sSL -o terraform_SHA256SUMS.sig https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_SHA256SUMS.${TERRAFORM_GPG_KEY}.sig + gpg --verify terraform_SHA256SUMS.sig terraform_SHA256SUMS + else + echo "${TERRAFORM_SHA256} *${terraform_filename}" > terraform_SHA256SUMS + fi + sha256sum --ignore-missing -c terraform_SHA256SUMS +fi +unzip ${terraform_filename} +mv -f terraform /usr/local/bin/ + +if [ "${TFLINT_VERSION}" != "none" ]; then + echo "Downloading tflint..." + TFLINT_FILENAME="tflint_linux_${architecture}.zip" + curl -sSL -o /tmp/tf-downloads/${TFLINT_FILENAME} https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/${TFLINT_FILENAME} + if [ "${TFLINT_SHA256}" != "dev-mode" ]; then + if [ "${TFLINT_SHA256}" = "automatic" ]; then + get_common_setting TFLINT_GPG_KEY_URI + curl -sSL -o tflint_key "${TFLINT_GPG_KEY_URI}" + gpg -q --import tflint_key + curl -sSL -o tflint_checksums.txt https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt + curl -sSL -o tflint_checksums.txt.sig https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.sig + gpg --verify tflint_checksums.txt.sig tflint_checksums.txt + else + echo "${TFLINT_SHA256} *${TFLINT_FILENAME}" > tflint_checksums.txt + fi + sha256sum --ignore-missing -c tflint_checksums.txt + fi + unzip /tmp/tf-downloads/${TFLINT_FILENAME} + mv -f tflint /usr/local/bin/ +fi +if [ "${TERRAGRUNT_VERSION}" != "none" ]; then + echo "Downloading Terragrunt..." + terragrunt_filename="terragrunt_linux_${architecture}" + curl -sSL -o /tmp/tf-downloads/${terragrunt_filename} https://github.com/gruntwork-io/terragrunt/releases/download/v${TERRAGRUNT_VERSION}/${terragrunt_filename} + if [ "${TERRAGRUNT_SHA256}" != "dev-mode" ]; then + if [ "${TERRAGRUNT_SHA256}" = "automatic" ]; then + curl -sSL -o terragrunt_SHA256SUMS https://github.com/gruntwork-io/terragrunt/releases/download/v${TERRAGRUNT_VERSION}/SHA256SUMS + else + echo "${TERRAGRUNT_SHA256} *${terragrunt_filename}" > terragrunt_SHA256SUMS + fi + sha256sum --ignore-missing -c terragrunt_SHA256SUMS + fi + chmod a+x /tmp/tf-downloads/${terragrunt_filename} + mv -f /tmp/tf-downloads/${terragrunt_filename} /usr/local/bin/terragrunt +fi + +rm -rf /tmp/tf-downloads ${GNUPGHOME} +echo "Done!" diff --git a/lib/utils.sh b/lib/utils.sh new file mode 100644 index 0000000..61dc20c --- /dev/null +++ b/lib/utils.sh @@ -0,0 +1,244 @@ +# If in automatic mode, determine if a user already exists, if not use root +detect_user() { + local user_variable_name=${1:-username} + local possible_users=${2:-("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)")} + if [ "${!user_variable_name}" = "auto" ] || [ "${!user_variable_name}" = "automatic" ]; then + declare -g ${user_variable_name}="" + for current_user in ${possible_users[@]}; do + if id -u "${current_user}" > /dev/null 2>&1; then + declare -g ${user_variable_name}="${current_user}" + break + fi + done + fi + if [ "${!user_variable_name}" = "" ] || [ "${!user_variable_name}" = "none" ] || ! id -u "${!user_variable_name}" > /dev/null 2>&1; then + declare -g ${user_variable_name}=root + fi +} + +# Use Oryx to install something using a partial version match +oryx_install() { + local platform=$1 + local requested_version=$2 + local target_folder=${3:-none} + local ldconfig_folder=${4:-none} + echo "(*) Installing ${platform} ${requested_version} using Oryx..." + check_packages jq + # Soft match if full version not specified + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local version_list="$(oryx platforms --json | jq -r ".[] | select(.Name == \"${platform}\") | .Versions | sort | reverse | @tsv" | tr '\t' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$')" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + requested_version="$(echo "${version_list}" | head -n 1)" + else + set +e + requested_version="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + if [ -z "${requested_version}" ] || ! echo "${version_list}" | grep "^${requested_version//./\\.}$" > /dev/null 2>&1; then + echo -e "(!) Oryx does not support ${platform} version $2\nValid values:\n${version_list}" >&2 + return 1 + fi + echo "(*) Using ${requested_version} in place of $2." + fi + + export ORYX_ENV_TYPE=vsonline-present ORYX_PREFER_USER_INSTALLED_SDKS=true ENABLE_DYNAMIC_INSTALL=true DYNAMIC_INSTALL_ROOT_DIR=/opt + oryx prep --skip-detection --platforms-and-versions "${platform}=${requested_version}" + local opt_folder="/opt/${platform}/${requested_version}" + if [ "${target_folder}" != "none" ] && [ "${target_folder}" != "${opt_folder}" ]; then + ln -s "${opt_folder}" "${target_folder}" + fi + # Update library path add to conf + if [ "${ldconfig_folder}" != "none" ]; then + echo "/opt/${platform}/${requested_version}/lib" >> "/etc/ld.so.conf.d/${platform}.conf" + ldconfig + fi +} + +# Use SDKMAN to install something using a partial version match +sdk_install() { + local install_type=$1 + local requested_version=$2 + local prefix=$3 + local suffix="${4:-"\\s*"}" + local full_version_check=${5:-".*-[a-z]+"} + if [ "${requested_version}" = "none" ]; then return; fi + # Blank will install latest stable version + if [ "${requested_version}" = "lts" ] || [ "${requested_version}" = "default" ]; then + requested_version="" + elif echo "${requested_version}" | grep -oE "${full_version_check}" > /dev/null 2>&1; then + echo "${requested_version}" + else + local regex="${prefix}\\K[0-9]+\\.[0-9]+\\.[0-9]+${suffix}" + local version_list="$(. ${SDKMAN_DIR}/bin/sdkman-init.sh && sdk list ${install_type} 2>&1 | grep -oP "${regex}" | tr -d ' ' | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ]; then + requested_version="$(echo "${version_list}" | head -n 1)" + else + set +e + requested_version="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + if [ -z "${requested_version}" ] || ! echo "${version_list}" | grep "^${requested_version//./\\.}$" > /dev/null 2>&1; then + echo -e "Version $2 not found. Available versions:\n${version_list}" >&2 + exit 1 + fi + fi + su ${USERNAME} -c "umask 0002 && . ${SDKMAN_DIR}/bin/sdkman-init.sh && sdk install ${install_type} ${requested_version} && sdk flush archives && sdk flush temp" +} + +updaterc() { + if [ "${UPDATE_RC}" = "true" ]; then + echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." + if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/bash.bashrc + fi + if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/zsh/zshrc + fi + fi +} + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Import the specified key in a variable name passed in as +receive_gpg_keys() { + get_common_setting $1 + local keys=${!1} + get_common_setting GPG_KEY_SERVERS true + local keyring_args="" + if [ ! -z "$2" ]; then + keyring_args="--no-default-keyring --keyring $2" + fi + + # Use a temporary locaiton for gpg keys to avoid polluting image + export GNUPGHOME="/tmp/tmp-gnupg" + mkdir -p ${GNUPGHOME} + chmod 700 ${GNUPGHOME} + echo -e "disable-ipv6\n${GPG_KEY_SERVERS}" > ${GNUPGHOME}/dirmngr.conf + # GPG key download sometimes fails for some reason and retrying fixes it. + local retry_count=0 + local gpg_ok="false" + set +e + until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; + do + echo "(*) Downloading GPG key..." + ( echo "${keys}" | xargs -n 1 gpg -q ${keyring_args} --recv-keys) 2>&1 && gpg_ok="true" + if [ "${gpg_ok}" != "true" ]; then + echo "(*) Failed getting key, retring in 10s..." + (( retry_count++ )) + sleep 10s + fi + done + set -e + if [ "${gpg_ok}" = "false" ]; then + echo "(!) Failed to get gpg key." + exit 1 + fi +} + +# Figure out correct version of a three part version number is not passed +find_version_from_git_tags() { + local variable_name=$1 + local requested_version=${!variable_name} + if [ "${requested_version}" = "none" ]; then return; fi + local repository=$2 + local prefix=${3:-"tags/v"} + local separator=${4:-"."} + local last_part_optional=${5:-"false"} + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local escaped_separator=${separator//./\\.} + local last_part + if [ "${last_part_optional}" = "true" ]; then + last_part="(${escaped_separator}[0-9]+)?" + else + last_part="${escaped_separator}[0-9]+" + fi + local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" + local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" + else + set +e + declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + fi + if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then + echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 + exit 1 + fi + echo "${variable_name}=${!variable_name}" +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Soft version matching that resolves a version for a given package in the *current apt-cache* +# Return value is stored in first argument (the unprocessed version) +apt_cache_version_soft_match() { + + # Version + local variable_name="$1" + local requested_version=${!variable_name} + # Package Name + local package_name="$2" + # Exit on no match? + local exit_on_no_match="${3:-true}" + + # Ensure we've exported useful variables + . /etc/os-release + local architecture="$(dpkg --print-architecture)" + + dot_escaped="${requested_version//./\\.}" + dot_plus_escaped="${dot_escaped//+/\\+}" + # Regex needs to handle debian package version number format: https://www.systutorials.com/docs/linux/man/5-deb-version/ + version_regex="^(.+:)?${dot_plus_escaped}([\\.\\+ ~:-]|$)" + set +e # Don't exit if finding version fails - handle gracefully + fuzzy_version="$(apt-cache madison ${package_name} | awk -F"|" '{print $2}' | sed -e 's/^[ \t]*//' | grep -E -m 1 "${version_regex}")" + set -e + if [ -z "${fuzzy_version}" ]; then + echo "(!) No full or partial for package \"${package_name}\" match found in apt-cache for \"${requested_version}\" on OS ${ID} ${VERSION_CODENAME} (${architecture})." + + if $exit_on_no_match; then + echo "Available versions:" + apt-cache madison ${package_name} | awk -F"|" '{print $2}' | grep -oP '^(.+:)?\K.+' + exit 1 # Fail entire script + else + echo "Continuing to fallback method (if available)" + return 1; + fi + fi + + # Globally assign fuzzy_version to this value + # Use this value as the return value of this function + declare -g ${variable_name}="=${fuzzy_version}" + echo "${variable_name}=${!variable_name}" +} diff --git a/settings.env b/settings.env new file mode 100644 index 0000000..cbad1ca --- /dev/null +++ b/settings.env @@ -0,0 +1,56 @@ +PYTHON_SOURCE_GPG_KEYS="64E628F8D684696D B26995E310250568 2D347EA6AA65421D FB9921286F5E1540 3A5CA953F73C700D 04C367C218ADD4FF 0EDDC5F26A45C816 6AF053F07D9DC8D2 C9BE28DEE6DF025C 126EB563A74B06BF D9866941EA5BBD71 ED9D77D5" +DEADSNAKES_PPA_ARCHIVE_GPG_KEY="F23C5A6CF475977595C89F51BA6932366A755776" +RVM_GPG_KEYS="409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB" +TERRAFORM_GPG_KEYS=72D7468F +GIT_CORE_PPA_ARCHIVE_GPG_KEY=E1DD270288B4E6030699E45FA1715D88E1DF1F24 +GITHUB_CLI_ARCHIVE_GPG_KEY=C99B11DEB97541F0 +GIT_LFS_ARCHIVE_GPG_KEY_URI="https://packagecloud.io/github/git-lfs/gpgkey" +GIT_LFS_ARCHIVE_ARCHITECTURES="amd64" +GIT_LFS_ARCHIVE_VERSION_CODENAMES="stretch buster bullseye bionic focal" +GIT_LFS_CHECKSUM_GPG_KEYS="0x88ace9b29196305ba9947552f1ba225c0223b187 0x86cd3297749375bcf8206715f54fe648088335a9 0xaa3b3450295830d2de6db90caba67be5a5795889" +POWERSHELL_ARCHIVE_ARCHITECTURES="amd64" +POWERSHELL_ARCHIVE_VERSION_CODENAMES="stretch buster bionic focal" +AZCLI_ARCHIVE_ARCHITECTURES="amd64" +AZCLI_ARCHIVE_VERSION_CODENAMES="stretch buster bullseye bionic focal" +DOTNET_ARCHIVE_ARCHITECTURES="amd64" +DOTNET_ARCHIVE_VERSION_CODENAMES="buster bullseye bionic focal hirsute" +DOTNET_CDN_FEED_URI="https://dotnetcli.azureedge.net" +HELM_GPG_KEYS_URI="https://raw.githubusercontent.com/helm/helm/main/KEYS" +MICROSOFT_GPG_KEYS_URI="https://packages.microsoft.com/keys/microsoft.asc" +TFLINT_GPG_KEY_URI="https://raw.githubusercontent.com/terraform-linters/tflint/master/8CE69160EB3F2FE9.key" +GO_GPG_KEY_URI="https://dl.google.com/linux/linux_signing_key.pub" +GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com:80 +keyserver hkps://keys.openpgp.org +keyserver hkp://keyserver.pgp.com" +AWSCLI_GPG_KEY=FB5DB77FD5C118B80511ADA8A6310ACC4672475C +AWSCLI_GPG_KEY_MATERIAL="-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBF2Cr7UBEADJZHcgusOJl7ENSyumXh85z0TRV0xJorM2B/JL0kHOyigQluUG +ZMLhENaG0bYatdrKP+3H91lvK050pXwnO/R7fB/FSTouki4ciIx5OuLlnJZIxSzx +PqGl0mkxImLNbGWoi6Lto0LYxqHN2iQtzlwTVmq9733zd3XfcXrZ3+LblHAgEt5G +TfNxEKJ8soPLyWmwDH6HWCnjZ/aIQRBTIQ05uVeEoYxSh6wOai7ss/KveoSNBbYz +gbdzoqI2Y8cgH2nbfgp3DSasaLZEdCSsIsK1u05CinE7k2qZ7KgKAUIcT/cR/grk +C6VwsnDU0OUCideXcQ8WeHutqvgZH1JgKDbznoIzeQHJD238GEu+eKhRHcz8/jeG +94zkcgJOz3KbZGYMiTh277Fvj9zzvZsbMBCedV1BTg3TqgvdX4bdkhf5cH+7NtWO +lrFj6UwAsGukBTAOxC0l/dnSmZhJ7Z1KmEWilro/gOrjtOxqRQutlIqG22TaqoPG +fYVN+en3Zwbt97kcgZDwqbuykNt64oZWc4XKCa3mprEGC3IbJTBFqglXmZ7l9ywG +EEUJYOlb2XrSuPWml39beWdKM8kzr1OjnlOm6+lpTRCBfo0wa9F8YZRhHPAkwKkX +XDeOGpWRj4ohOx0d2GWkyV5xyN14p2tQOCdOODmz80yUTgRpPVQUtOEhXQARAQAB +tCFBV1MgQ0xJIFRlYW0gPGF3cy1jbGlAYW1hem9uLmNvbT6JAlQEEwEIAD4WIQT7 +Xbd/1cEYuAURraimMQrMRnJHXAUCXYKvtQIbAwUJB4TOAAULCQgHAgYVCgkICwIE +FgIDAQIeAQIXgAAKCRCmMQrMRnJHXJIXEAChLUIkg80uPUkGjE3jejvQSA1aWuAM +yzy6fdpdlRUz6M6nmsUhOExjVIvibEJpzK5mhuSZ4lb0vJ2ZUPgCv4zs2nBd7BGJ +MxKiWgBReGvTdqZ0SzyYH4PYCJSE732x/Fw9hfnh1dMTXNcrQXzwOmmFNNegG0Ox +au+VnpcR5Kz3smiTrIwZbRudo1ijhCYPQ7t5CMp9kjC6bObvy1hSIg2xNbMAN/Do +ikebAl36uA6Y/Uczjj3GxZW4ZWeFirMidKbtqvUz2y0UFszobjiBSqZZHCreC34B +hw9bFNpuWC/0SrXgohdsc6vK50pDGdV5kM2qo9tMQ/izsAwTh/d/GzZv8H4lV9eO +tEis+EpR497PaxKKh9tJf0N6Q1YLRHof5xePZtOIlS3gfvsH5hXA3HJ9yIxb8T0H +QYmVr3aIUes20i6meI3fuV36VFupwfrTKaL7VXnsrK2fq5cRvyJLNzXucg0WAjPF +RrAGLzY7nP1xeg1a0aeP+pdsqjqlPJom8OCWc1+6DWbg0jsC74WoesAqgBItODMB +rsal1y/q+bPzpsnWjzHV8+1/EtZmSc8ZUGSJOPkfC7hObnfkl18h+1QtKTjZme4d +H17gsBJr+opwJw/Zio2LMjQBOqlm3K1A4zFTh7wBC7He6KPQea1p2XAMgtvATtNe +YLZATHZKTJyiqA== +=vYOk +-----END PGP PUBLIC KEY BLOCK-----" +DOCKER_IN_DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES="buster bullseye bionic focal jammy" +DOCKER_IN_DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES="buster bullseye bionic focal hirsute impish jammy" \ No newline at end of file