From 685fa2e0f151b4545b444135a5ef6877de20e851 Mon Sep 17 00:00:00 2001 From: Krikchai Pongtaveewould Date: Thu, 3 Aug 2023 05:12:14 +0700 Subject: [PATCH 01/25] Fix: common-utils configureZshAsDefaultShell option not working on alpine linux image (#557) * alpine defaultZsh fix * common-utils: replace pam.d/chsh permission with `sufficient` * common-utils: revert match auth sufficient * common-utils: append auth string instead of replacing a whole file * fix grep 2nd param * fix: line checking condition --- src/common-utils/devcontainer-feature.json | 2 +- src/common-utils/main.sh | 8 ++++++++ test/common-utils/alpine-base-zsh-default.sh | 13 +++++++++++++ test/common-utils/scenarios.json | 13 +++++++++++-- 4 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 test/common-utils/alpine-base-zsh-default.sh diff --git a/src/common-utils/devcontainer-feature.json b/src/common-utils/devcontainer-feature.json index aadc1bc..a924aa7 100644 --- a/src/common-utils/devcontainer-feature.json +++ b/src/common-utils/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "common-utils", - "version": "2.0.10", + "version": "2.0.11", "name": "Common Utilities", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/common-utils", "description": "Installs a set of common command line utilities, Oh My Zsh!, and sets up a non-root user.", diff --git a/src/common-utils/main.sh b/src/common-utils/main.sh index 144478e..6d5ac07 100644 --- a/src/common-utils/main.sh +++ b/src/common-utils/main.sh @@ -437,6 +437,14 @@ if [ "${INSTALL_ZSH}" = "true" ]; then fi if [ "${CONFIGURE_ZSH_AS_DEFAULT_SHELL}" == "true" ]; then + # Fixing chsh always asking for a password on alpine linux + # ref: https://askubuntu.com/questions/812420/chsh-always-asking-a-password-and-get-pam-authentication-failure. + if [ ! -f "/etc/pam.d/chsh" ] || ! grep -Eq '^auth(.*)pam_rootok\.so$' /etc/pam.d/chsh; then + echo "auth sufficient pam_rootok.so" >> /etc/pam.d/chsh + elif [[ -n "$(awk '/^auth(.*)pam_rootok\.so$/ && !/^auth[[:blank:]]+sufficient[[:blank:]]+pam_rootok\.so$/' /etc/pam.d/chsh)" ]]; then + awk '/^auth(.*)pam_rootok\.so$/ { $2 = "sufficient" } { print }' /etc/pam.d/chsh > /tmp/chsh.tmp && mv /tmp/chsh.tmp /etc/pam.d/chsh + fi + chsh --shell /bin/zsh ${USERNAME} fi diff --git a/test/common-utils/alpine-base-zsh-default.sh b/test/common-utils/alpine-base-zsh-default.sh new file mode 100644 index 0000000..9fc00a2 --- /dev/null +++ b/test/common-utils/alpine-base-zsh-default.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Definition specific tests +check "alpine default shell zsh" \ + bash -c "getent passwd $(whoami) | awk -F : '{ print $7 }' | grep '/bin/zsh'" + +# Report result +reportResults diff --git a/test/common-utils/scenarios.json b/test/common-utils/scenarios.json index 21cb940..9800242 100644 --- a/test/common-utils/scenarios.json +++ b/test/common-utils/scenarios.json @@ -143,6 +143,7 @@ "build": { "dockerfile": "Dockerfile" }, + "remoteUser": "vscode", "features": { "common-utils": { "username": "vscode", @@ -151,7 +152,15 @@ "upgradePackages": true, "installZsh": true } - }, - "remoteUser": "vscode" + } + }, + "alpine-base-zsh-default": { + "image": "mcr.microsoft.com/devcontainers/base:alpine", + "remoteUser": "vscode", + "features": { + "common-utils": { + "configureZshAsDefaultShell": true + } + } } } From 29859aff280dc187d6f508900f8600ebd04c56b7 Mon Sep 17 00:00:00 2001 From: Tom Palmer Date: Mon, 7 Aug 2023 23:12:13 +0100 Subject: [PATCH 02/25] Fix 2 instances of a typo in the desktop-lite NOTES and README (#640) * Amend 'is installed is' to 'installed is'. * Undo typo fix from README Co-authored-by: Samruddhi Khandale --------- Co-authored-by: Samruddhi Khandale --- src/desktop-lite/NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/desktop-lite/NOTES.md b/src/desktop-lite/NOTES.md index 9080c43..924ce9b 100644 --- a/src/desktop-lite/NOTES.md +++ b/src/desktop-lite/NOTES.md @@ -25,7 +25,7 @@ You can also connect to the desktop using a [VNC viewer](https://www.realvnc.com ## Customizing Fluxbox -The window manager is installed is [Fluxbox](http://fluxbox.org/). **Right-click** to see the application menu. In addition, any UI-based commands you execute inside the dev container will automatically appear on the desktop. +The window manager installed is [Fluxbox](http://fluxbox.org/). **Right-click** to see the application menu. In addition, any UI-based commands you execute inside the dev container will automatically appear on the desktop. You can customize the desktop using Fluxbox configuration files. The configuration files are located in the `.fluxbox` folder of the home directory of the user you using to connect to the dev container (`$HOME/.fluxbox`). From bbb24d10d417452ef23a8c506fd0e2b141b71f15 Mon Sep 17 00:00:00 2001 From: Samruddhi Khandale Date: Mon, 7 Aug 2023 15:59:58 -0700 Subject: [PATCH 03/25] Docker-in-docker: Add retries to the docker-init script until docker daemon starts (#637) * Docker-in-docker: Add retries until docker daemon starts * address comments --- .../devcontainer-feature.json | 2 +- src/docker-in-docker/install.sh | 30 +++++++++++++---- test/docker-in-docker/docker_retry.sh | 32 +++++++++++++++++++ test/docker-in-docker/scenarios.json | 7 +++- .../test-scripts/docker-test-init.sh | 26 +++++++++++++++ 5 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 test/docker-in-docker/docker_retry.sh create mode 100644 test/docker-in-docker/test-scripts/docker-test-init.sh diff --git a/src/docker-in-docker/devcontainer-feature.json b/src/docker-in-docker/devcontainer-feature.json index 3349821..b5e0aee 100644 --- a/src/docker-in-docker/devcontainer-feature.json +++ b/src/docker-in-docker/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "docker-in-docker", - "version": "2.2.1", + "version": "2.3.0", "name": "Docker (Docker-in-Docker)", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/docker-in-docker", "description": "Create child containers *inside* a container, independent from the host's docker instance. Installs Docker extension in the container along with needed CLIs.", diff --git a/src/docker-in-docker/install.sh b/src/docker-in-docker/install.sh index 6c889f5..f7e5a7d 100755 --- a/src/docker-in-docker/install.sh +++ b/src/docker-in-docker/install.sh @@ -5,7 +5,7 @@ #------------------------------------------------------------------------------------------------------------- # # Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker-in-docker.md -# Maintainer: The VS Code and Codespaces Teams +# Maintainer: The Dev Container spec maintainers DOCKER_VERSION="${VERSION:-"latest"}" # The Docker/Moby Engine + CLI should match in version @@ -423,12 +423,28 @@ dockerd_start="AZURE_DNS_AUTO_DETECTION=${AZURE_DNS_AUTO_DETECTION} DOCKER_DEFAU 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 +retry_count=0 +docker_ok="false" + +until [ "${docker_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; +do + # 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 + docker info > /dev/null 2>&1 && docker_ok="true" + + if [ "${docker_ok}" != "true" ]; then + echo "(*) Failed to start docker, retrying in 5s..." + retry_count=`expr $retry_count + 1` + sleep 5s + fi + set -e +done set +e diff --git a/test/docker-in-docker/docker_retry.sh b/test/docker-in-docker/docker_retry.sh new file mode 100644 index 0000000..cc35fbb --- /dev/null +++ b/test/docker-in-docker/docker_retry.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Definition specific tests +check "docker-buildx" docker buildx version +check "docker-ps" docker ps + +sleep 5s + +# Stop docker +pkill dockerd +pkill containerd + +sleep 5s + +set +e + docker_ok_code="$(docker info > /dev/null 2>&1; echo $?)" +set -e + +check "docker-not-running" bash -c "[[ ${docker_ok_code} == 1 ]]" + +# Testing retry logic +./test-scripts/docker-test-init.sh + +check "docker-started-after-retries" docker ps + +# Report result +reportResults diff --git a/test/docker-in-docker/scenarios.json b/test/docker-in-docker/scenarios.json index 9c44478..525ce69 100644 --- a/test/docker-in-docker/scenarios.json +++ b/test/docker-in-docker/scenarios.json @@ -67,6 +67,11 @@ } }, "remoteUser": "node" + }, + "docker_retry": { + "image": "ubuntu:focal", + "features": { + "docker-in-docker": {} + } } - } diff --git a/test/docker-in-docker/test-scripts/docker-test-init.sh b/test/docker-in-docker/test-scripts/docker-test-init.sh new file mode 100644 index 0000000..b213fb4 --- /dev/null +++ b/test/docker-in-docker/test-scripts/docker-test-init.sh @@ -0,0 +1,26 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------- + +retry_count=0 +docker_ok="false" + +until [ "${docker_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; +do + if [ "${retry_count}" -eq "3" ]; then + echo "Starting docker after 3 retries..." + /usr/local/share/docker-init.sh + fi + + set +e + docker info > /dev/null 2>&1 && docker_ok="true" + + if [ "${docker_ok}" != "true" ]; then + echo "(*) Failed to start docker, retrying in 5s... Retry count: ${retry_count}" + retry_count=`expr $retry_count + 1` + sleep 1s + fi + set -e +done \ No newline at end of file From f2594c5cc85e18450a31865f8387c8dc064006f2 Mon Sep 17 00:00:00 2001 From: Samruddhi Khandale Date: Mon, 7 Aug 2023 16:29:54 -0700 Subject: [PATCH 04/25] Automated documentation update (#642) Automated documentation update [skip ci] Co-authored-by: github-actions --- src/desktop-lite/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/desktop-lite/README.md b/src/desktop-lite/README.md index 4839792..7094a24 100644 --- a/src/desktop-lite/README.md +++ b/src/desktop-lite/README.md @@ -48,7 +48,7 @@ You can also connect to the desktop using a [VNC viewer](https://www.realvnc.com ## Customizing Fluxbox -The window manager is installed is [Fluxbox](http://fluxbox.org/). **Right-click** to see the application menu. In addition, any UI-based commands you execute inside the dev container will automatically appear on the desktop. +The window manager installed is [Fluxbox](http://fluxbox.org/). **Right-click** to see the application menu. In addition, any UI-based commands you execute inside the dev container will automatically appear on the desktop. You can customize the desktop using Fluxbox configuration files. The configuration files are located in the `.fluxbox` folder of the home directory of the user you using to connect to the dev container (`$HOME/.fluxbox`). From 9e75db9a1dbcc245f94707c0917470143c17595b Mon Sep 17 00:00:00 2001 From: Shikanime Deva Date: Thu, 10 Aug 2023 01:20:27 +0200 Subject: [PATCH 05/25] Avoid unexpected shell expansion (#645) * Avoid unexpected shell expansion * Add install Node with ZSH as default test * Fix LTS only work on >bionic * Bump Node feature version * Remove huge universal image on scenarios --- src/node/devcontainer-feature.json | 4 ++-- src/node/install.sh | 24 ++++++++++++------------ test/node/scenarios.json | 22 ++++++++++++---------- test/node/zsh_default.sh | 12 ++++++++++++ 4 files changed, 38 insertions(+), 24 deletions(-) create mode 100755 test/node/zsh_default.sh diff --git a/src/node/devcontainer-feature.json b/src/node/devcontainer-feature.json index 9bf90b6..80bd912 100644 --- a/src/node/devcontainer-feature.json +++ b/src/node/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "node", - "version": "1.2.0", + "version": "1.2.1", "name": "Node.js (via nvm), yarn and pnpm", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/node", "description": "Installs Node.js, nvm, yarn, pnpm, and needed dependencies.", @@ -49,4 +49,4 @@ "installsAfter": [ "ghcr.io/devcontainers/features/common-utils" ] -} +} \ No newline at end of file diff --git a/src/node/install.sh b/src/node/install.sh index 9048ac2..1ce8fee 100755 --- a/src/node/install.sh +++ b/src/node/install.sh @@ -9,7 +9,7 @@ export NODE_VERSION="${VERSION:-"lts"}" export NVM_VERSION="${NVMVERSION:-"0.39.2"}" -export NVM_DIR=${NVMINSTALLPATH:-"/usr/local/share/nvm"} +export NVM_DIR="${NVMINSTALLPATH:-"/usr/local/share/nvm"}" INSTALL_TOOLS_FOR_NODE_GYP="${NODEGYPDEPENDENCIES:-true}" # Comma-separated list of node versions to be installed (with nvm) @@ -118,10 +118,10 @@ 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 +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} + nvm alias default "${NODE_VERSION}" fi EOF )" @@ -149,9 +149,9 @@ usermod -a -G nvm ${USERNAME} umask 0002 if [ ! -d "${NVM_DIR}" ]; then # Create nvm dir, and set sticky bit - mkdir -p ${NVM_DIR} - chown "${USERNAME}:nvm" ${NVM_DIR} - chmod g+rws ${NVM_DIR} + mkdir -p "${NVM_DIR}" + chown "${USERNAME}:nvm" "${NVM_DIR}" + chmod g+rws "${NVM_DIR}" su ${USERNAME} -c "${nvm_install_snippet}" 2>&1 # Update rc files if [ "${UPDATE_RC}" = "true" ]; then @@ -160,11 +160,11 @@ if [ ! -d "${NVM_DIR}" ]; then else echo "NVM already installed." if [ "${NODE_VERSION}" != "" ]; then - su ${USERNAME} -c "umask 0002 && . $NVM_DIR/nvm.sh && nvm install ${NODE_VERSION} && nvm alias default ${NODE_VERSION}" + su ${USERNAME} -c "umask 0002 && . '$NVM_DIR/nvm.sh' && nvm install '${NODE_VERSION}' && nvm alias default '${NODE_VERSION}'" fi fi -# Additional node versions to be installed but not be set as +# Additional node versions to be installed but not be set as # default we can assume the nvm is the group owner of the nvm # directory and the sticky bit on directories so any installed # files will have will have the correct ownership (nvm) @@ -173,12 +173,12 @@ if [ ! -z "${ADDITIONAL_VERSIONS}" ]; then IFS="," read -a additional_versions <<< "$ADDITIONAL_VERSIONS" for ver in "${additional_versions[@]}"; do - su ${USERNAME} -c "umask 0002 && . $NVM_DIR/nvm.sh && nvm install ${ver}" + su ${USERNAME} -c "umask 0002 && . '$NVM_DIR/nvm.sh' && nvm install '${ver}'" done # Ensure $NODE_VERSION is on the $PATH if [ "${NODE_VERSION}" != "" ]; then - su ${USERNAME} -c "umask 0002 && . $NVM_DIR/nvm.sh && nvm use default" + su ${USERNAME} -c "umask 0002 && . '$NVM_DIR/nvm.sh' && nvm use default" fi IFS=$OLDIFS fi @@ -218,7 +218,7 @@ fi # Clean up -su ${USERNAME} -c "umask 0002 && . $NVM_DIR/nvm.sh && nvm clear-cache" +su ${USERNAME} -c "umask 0002 && . '$NVM_DIR/nvm.sh' && nvm clear-cache" rm -rf /var/lib/apt/lists/* # Ensure privs are correct for installed node versions. Unfortunately the diff --git a/test/node/scenarios.json b/test/node/scenarios.json index 6112a6e..5524f5c 100644 --- a/test/node/scenarios.json +++ b/test/node/scenarios.json @@ -17,6 +17,17 @@ } } }, + "zsh_default": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "node": { + "version": "lts" + }, + "common-utils": { + "configureZshAsDefaultShell": true + } + } + }, "version_none": { "image": "mcr.microsoft.com/devcontainers/base", "remoteUser": "vscode", @@ -26,15 +37,6 @@ } } }, - "install_node_on_universal_image": { - "image": "mcr.microsoft.com/devcontainers/universal", - "remoteUser": "codespace", - "features": { - "node": { - "version": "19.1.0" - } - } - }, "install_node_16_on_bionic": { "image": "mcr.microsoft.com/devcontainers/base:ubuntu-18.04", "features": { @@ -43,4 +45,4 @@ } } } -} +} \ No newline at end of file diff --git a/test/node/zsh_default.sh b/test/node/zsh_default.sh new file mode 100755 index 0000000..c7c9e57 --- /dev/null +++ b/test/node/zsh_default.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Definition specific tests +check "nvm" bash -c ". /usr/local/share/nvm/nvm.sh && nvm install 10" + +# Report result +reportResults From be082b0ef5f0f3795708cd1a6c37a427d8c0bb73 Mon Sep 17 00:00:00 2001 From: naturedamends <120284608+naturedamends@users.noreply.github.com> Date: Sat, 12 Aug 2023 00:01:57 +0100 Subject: [PATCH 06/25] Common utils: Add config to remove zsh rc files from (#614) * Add config to remove zsh rc files from common-utils. * Bump version and add config to install script. * Checks preventing excess writing to .zshrc. * Change devcontainer feature option name. * Coding standards. * Favour adding feature via config and default to overriding .zshrc with dev-container default template. * Update devcontainer-feature.json * Update devcontainer-feature.json * Update src/common-utils/devcontainer-feature.json Co-authored-by: Samruddhi Khandale * Update src/common-utils/devcontainer-feature.json Co-authored-by: Samruddhi Khandale * Testing for using devcontainer .zshrc template file. * Coding standards. * Coding standards. * Update configure_zsh_as_default_shell_no_template.sh * Grammar in configure_zsh_as_default_shell.sh * Testing accounts for marked file cache (#4) * Account for mark file in testing. * Remove some debugging and tests back * Add back tests? * Update configure_zsh_no_template.sh --------- Co-authored-by: Samruddhi Khandale --- src/common-utils/devcontainer-feature.json | 7 ++- src/common-utils/install.sh | 1 + src/common-utils/main.sh | 51 +++++++++++-------- .../configure_zsh_as_default_shell.sh | 2 + .../common-utils/configure_zsh_no_template.sh | 15 ++++++ test/common-utils/scenarios.json | 11 ++++ 6 files changed, 66 insertions(+), 21 deletions(-) create mode 100644 test/common-utils/configure_zsh_no_template.sh diff --git a/src/common-utils/devcontainer-feature.json b/src/common-utils/devcontainer-feature.json index a924aa7..d708482 100644 --- a/src/common-utils/devcontainer-feature.json +++ b/src/common-utils/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "common-utils", - "version": "2.0.11", + "version": "2.1.0", "name": "Common Utilities", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/common-utils", "description": "Installs a set of common command line utilities, Oh My Zsh!, and sets up a non-root user.", @@ -20,6 +20,11 @@ "default": true, "description": "Install Oh My Zsh!?" }, + "installOhMyZshConfig": { + "type": "boolean", + "default": true, + "description": "Allow installing the default dev container .zshrc templates?" + }, "upgradePackages": { "type": "boolean", "default": true, diff --git a/src/common-utils/install.sh b/src/common-utils/install.sh index d08a2f2..8f1ece4 100755 --- a/src/common-utils/install.sh +++ b/src/common-utils/install.sh @@ -12,6 +12,7 @@ set -e INSTALL_ZSH="${INSTALLZSH:-"true"}" CONFIGURE_ZSH_AS_DEFAULT_SHELL="${CONFIGUREZSHASDEFAULTSHELL:-"false"}" INSTALL_OH_MY_ZSH="${INSTALLOHMYZSH:-"true"}" +INSTALL_OH_MY_ZSH_CONFIG="${INSTALLOHMYZSHCONFIG:-"true"}" UPGRADE_PACKAGES="${UPGRADEPACKAGES:-"true"}" USERNAME="${USERNAME:-"automatic"}" USER_UID="${UID:-"automatic"}" diff --git a/src/common-utils/main.sh b/src/common-utils/main.sh index 6d5ac07..d3f7ef2 100644 --- a/src/common-utils/main.sh +++ b/src/common-utils/main.sh @@ -12,6 +12,7 @@ set -e INSTALL_ZSH="${INSTALLZSH:-"true"}" CONFIGURE_ZSH_AS_DEFAULT_SHELL="${CONFIGUREZSHASDEFAULTSHELL:-"false"}" INSTALL_OH_MY_ZSH="${INSTALLOHMYZSH:-"true"}" +INSTALL_OH_MY_ZSH_CONFIG="${INSTALLOHMYZSHCONFIG:-"true"}" UPGRADE_PACKAGES="${UPGRADEPACKAGES:-"true"}" USERNAME="${USERNAME:-"automatic"}" USER_UID="${USERUID:-"automatic"}" @@ -390,7 +391,8 @@ else fi # Restore user .bashrc / .profile / .zshrc defaults from skeleton file if it doesn't exist or is empty -possible_rc_files=( ".bashrc" ".profile" ".zshrc" ) +possible_rc_files=( ".bashrc" ".profile" ) +[ "$INSTALL_OH_MY_ZSH_CONFIG" == "true" ] && possible_rc_files+=('.zshrc') for rc_file in "${possible_rc_files[@]}"; do if [ -f "/etc/skel/${rc_file}" ]; then if [ ! -e "${user_home}/${rc_file}" ] || [ ! -s "${user_home}/${rc_file}" ]; then @@ -450,33 +452,42 @@ if [ "${INSTALL_ZSH}" = "true" ]; then # 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_home}/.oh-my-zsh" - if [ ! -d "${oh_my_install_dir}" ] && [ "${INSTALL_OH_MY_ZSH}" = "true" ]; then - template_path="${oh_my_install_dir}/templates/zshrc.zsh-template" + if [ "${INSTALL_OH_MY_ZSH}" = "true" ]; then user_rc_file="${user_home}/.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="devcontainers"/g' ${user_rc_file} + oh_my_install_dir="${user_home}/.oh-my-zsh" + template_path="${oh_my_install_dir}/templates/zshrc.zsh-template" + if [ ! -d "${oh_my_install_dir}" ]; then + 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 + + # Shrink git while still enabling updates + cd "${oh_my_install_dir}" + git repack -a -d -f --depth=1 --window=1 + fi # Add Dev Containers theme mkdir -p ${oh_my_install_dir}/custom/themes cp -f "${FEATURE_DIR}/scripts/devcontainers.zsh-theme" "${oh_my_install_dir}/custom/themes/devcontainers.zsh-theme" - ln -s "${oh_my_install_dir}/custom/themes/devcontainers.zsh-theme" "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme" + ln -sf "${oh_my_install_dir}/custom/themes/devcontainers.zsh-theme" "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme" + + # Add devcontainer .zshrc template + if [ "$INSTALL_OH_MY_ZSH_CONFIG" = "true" ]; then + echo -e "$(cat "${template_path}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${user_rc_file} + sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="devcontainers"/g' ${user_rc_file} + fi - # 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 + copy_to_user_files=("${oh_my_install_dir}") + [ -f "$user_rc_file" ] && copy_to_user_files+=("$user_rc_file") + cp -rf "${copy_to_user_files[@]}" /root chown -R ${USERNAME}:${group_name} "${oh_my_install_dir}" "${user_rc_file}" fi fi diff --git a/test/common-utils/configure_zsh_as_default_shell.sh b/test/common-utils/configure_zsh_as_default_shell.sh index e5cc89e..6a569a9 100644 --- a/test/common-utils/configure_zsh_as_default_shell.sh +++ b/test/common-utils/configure_zsh_as_default_shell.sh @@ -7,6 +7,8 @@ source dev-container-features-test-lib # Definition specific tests check "default-shell-is-zsh" bash -c "getent passwd $(whoami) | awk -F: '{ print $7 }' | grep '/bin/zsh'" +# check it overrides the ~/.zshrc with default dev containers template +check "default-zshrc-is-dev-container-template" bash -c "cat ~/.zshrc | grep ZSH_THEME | grep devcontainers" # Report result reportResults diff --git a/test/common-utils/configure_zsh_no_template.sh b/test/common-utils/configure_zsh_no_template.sh new file mode 100644 index 0000000..c62cae9 --- /dev/null +++ b/test/common-utils/configure_zsh_no_template.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Definition specific tests +function file_not_overridden() { + cat ~/.zshrc | grep 'alias fnomockalias=' | grep testingmock +} +check "default-zsh-with-no-zshrc" file_not_overridden + +# Report result +reportResults diff --git a/test/common-utils/scenarios.json b/test/common-utils/scenarios.json index 9800242..904a47b 100644 --- a/test/common-utils/scenarios.json +++ b/test/common-utils/scenarios.json @@ -115,6 +115,17 @@ } } }, + "configure_zsh_no_template": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "postCreateCommand": "echo alias fnomockalias=testingmock >> /root/.zshrc", + "remoteUser": "root", + "features": { + "common-utils": { + "installZsh": true, + "installOhMyZshConfig": false + } + } + }, "config-subdirectory": { "image": "alpine", "remoteUser": "devcontainer", From 8b6f08825643e2d5835ed3138dee5301a4db4ffa Mon Sep 17 00:00:00 2001 From: Samruddhi Khandale Date: Fri, 11 Aug 2023 16:17:50 -0700 Subject: [PATCH 07/25] Automated documentation update (#650) Automated documentation update [skip ci] Co-authored-by: github-actions --- src/common-utils/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common-utils/README.md b/src/common-utils/README.md index 1eddcbc..b8e37a8 100644 --- a/src/common-utils/README.md +++ b/src/common-utils/README.md @@ -18,6 +18,7 @@ Installs a set of common command line utilities, Oh My Zsh!, and sets up a non-r | installZsh | Install ZSH? | boolean | true | | configureZshAsDefaultShell | Change default shell to ZSH? | boolean | false | | installOhMyZsh | Install Oh My Zsh!? | boolean | true | +| installOhMyZshConfig | Allow installing the default dev container .zshrc templates? | boolean | true | | upgradePackages | Upgrade OS packages? | boolean | true | | username | Enter name of a non-root user to configure or none to skip | string | automatic | | userUid | Enter UID for non-root user | string | automatic | From f90cb26c7f15659f3e2be8061295997df2bb76cc Mon Sep 17 00:00:00 2001 From: Samruddhi Khandale Date: Mon, 14 Aug 2023 16:12:47 -0700 Subject: [PATCH 08/25] Common-utils: Fixes "No package jq available" errors for CentOS (#644) * add checks for "jq" * install epel-release if necessary * clean up epel-release post installation of "jq" --- src/common-utils/devcontainer-feature.json | 2 +- src/common-utils/main.sh | 18 +++++++++++++++--- test/common-utils/centos-7.sh | 1 + test/common-utils/fedora.sh | 1 + test/common-utils/mariner.sh | 1 + test/common-utils/rocky-8.sh | 1 + test/common-utils/rocky-9.sh | 1 + 7 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/common-utils/devcontainer-feature.json b/src/common-utils/devcontainer-feature.json index d708482..4eaef21 100644 --- a/src/common-utils/devcontainer-feature.json +++ b/src/common-utils/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "common-utils", - "version": "2.1.0", + "version": "2.1.1", "name": "Common Utilities", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/common-utils", "description": "Installs a set of common command line utilities, Oh My Zsh!, and sets up a non-root user.", diff --git a/src/common-utils/main.sh b/src/common-utils/main.sh index d3f7ef2..3731994 100644 --- a/src/common-utils/main.sh +++ b/src/common-utils/main.sh @@ -175,6 +175,11 @@ install_redhat_packages() { man-db \ strace" + local install_cmd=dnf + if ! type dnf > /dev/null 2>&1; then + install_cmd=yum + fi + # rockylinux:9 installs 'curl-minimal' which clashes with 'curl' # Install 'curl' for every OS except this rockylinux:9 if [[ "${ID}" = "rocky" ]] && [[ "${VERSION}" != *"9."* ]]; then @@ -201,16 +206,23 @@ install_redhat_packages() { package_list="${package_list} zsh" fi - local install_cmd=dnf - if ! type dnf > /dev/null 2>&1; then - install_cmd=yum + # Install EPEL repository if needed (required to install 'jq' for CentOS) + local remove_epel="false" + if ! ${install_cmd} -q list jq >/dev/null 2>&1; then + ${install_cmd} -y install epel-release + remove_epel="true" fi + ${install_cmd} -y install ${package_list} # Get to latest versions of all packages if [ "${UPGRADE_PACKAGES}" = "true" ]; then ${install_cmd} upgrade -y fi + + if [[ "${remove_epel}" = "true" ]]; then + ${install_cmd} -y remove epel-release + fi } # Alpine Linux packages diff --git a/test/common-utils/centos-7.sh b/test/common-utils/centos-7.sh index a81ae2f..69f01d1 100755 --- a/test/common-utils/centos-7.sh +++ b/test/common-utils/centos-7.sh @@ -9,6 +9,7 @@ source dev-container-features-test-lib . /etc/os-release check "non-root user" test "$(whoami)" = "devcontainer" check "distro" test "${VERSION_ID}" = "7" +check "jq" jq --version # Report result reportResults \ No newline at end of file diff --git a/test/common-utils/fedora.sh b/test/common-utils/fedora.sh index b32a7f1..67706d4 100755 --- a/test/common-utils/fedora.sh +++ b/test/common-utils/fedora.sh @@ -9,6 +9,7 @@ source dev-container-features-test-lib . /etc/os-release check "non-root user" test "$(whoami)" = "devcontainer" check "distro" test "${ID}" = "fedora" +check "jq" jq --version # Report result reportResults \ No newline at end of file diff --git a/test/common-utils/mariner.sh b/test/common-utils/mariner.sh index ab6cb6e..b0d2c91 100755 --- a/test/common-utils/mariner.sh +++ b/test/common-utils/mariner.sh @@ -9,6 +9,7 @@ source dev-container-features-test-lib . /etc/os-release check "non-root user" test "$(whoami)" = "devcontainer" check "distro" test "${ID}" = "mariner" +check "jq" jq --version # Report result reportResults \ No newline at end of file diff --git a/test/common-utils/rocky-8.sh b/test/common-utils/rocky-8.sh index be1223b..d087318 100755 --- a/test/common-utils/rocky-8.sh +++ b/test/common-utils/rocky-8.sh @@ -10,6 +10,7 @@ source dev-container-features-test-lib check "non-root user" test "$(whoami)" = "devcontainer" check "distro" test "${PLATFORM_ID}" = "platform:el8" check "curl" curl --version +check "jq" jq --version # Report result reportResults \ No newline at end of file diff --git a/test/common-utils/rocky-9.sh b/test/common-utils/rocky-9.sh index 452c193..cb2b339 100755 --- a/test/common-utils/rocky-9.sh +++ b/test/common-utils/rocky-9.sh @@ -10,6 +10,7 @@ source dev-container-features-test-lib check "non-root user" test "$(whoami)" = "devcontainer" check "distro" test "${PLATFORM_ID}" = "platform:el9" check "curl" curl --version +check "jq" jq --version # Report result reportResults \ No newline at end of file From 1d1a23625dc714629ab863cbca7e890516d4283d Mon Sep 17 00:00:00 2001 From: Samruddhi Khandale Date: Wed, 16 Aug 2023 11:38:35 -0700 Subject: [PATCH 09/25] Ruby Feature [bug fix] : Fails if the dev container has the same ruby version installed (#609) * Ruby Feature [bug fix] : Fails if the dev container has the same ruby version installed * add comment * remove scenario --- src/ruby/devcontainer-feature.json | 2 +- src/ruby/install.sh | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/ruby/devcontainer-feature.json b/src/ruby/devcontainer-feature.json index fdf8b26..29591e6 100644 --- a/src/ruby/devcontainer-feature.json +++ b/src/ruby/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "ruby", - "version": "1.1.0", + "version": "1.1.1", "name": "Ruby (via rvm)", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/ruby", "description": "Installs Ruby, rvm, rbenv, common Ruby utilities, and needed dependencies.", diff --git a/src/ruby/install.sh b/src/ruby/install.sh index 0871085..2b47d8a 100755 --- a/src/ruby/install.sh +++ b/src/ruby/install.sh @@ -177,9 +177,11 @@ fi find_version_from_git_tags RUBY_VERSION "https://github.com/ruby/ruby" "tags/v" "_" # Just install Ruby if RVM already installed -if [ $(rvm --version) != "" ]; then +if rvm --version > /dev/null; then echo "Ruby Version Manager already exists." - if [ "${RUBY_VERSION}" != "none" ]; then + if [[ "$(ruby -v)" = *"${RUBY_VERSION}"* ]]; then + echo "(!) Ruby is already installed with version ${RUBY_VERSION}. Skipping..." + elif [ "${RUBY_VERSION}" != "none" ]; then echo "Installing specified Ruby version." su ${USERNAME} -c "rvm install ruby ${RUBY_VERSION}" fi @@ -191,6 +193,9 @@ else # Determine appropriate settings for rvm installer if [ "${RUBY_VERSION}" = "none" ]; then RVM_INSTALL_ARGS="" + elif [[ "$(ruby -v)" = *"${RUBY_VERSION}"* ]]; then + echo "(!) Ruby is already installed with version ${RUBY_VERSION}. Skipping..." + RVM_INSTALL_ARGS="" else if [ "${RUBY_VERSION}" = "latest" ] || [ "${RUBY_VERSION}" = "current" ] || [ "${RUBY_VERSION}" = "lts" ]; then RVM_INSTALL_ARGS="--ruby" @@ -273,8 +278,11 @@ if [ "${SKIP_RBENV_RBUILD}" != "true" ]; then ln -s /usr/local/share/ruby-build /home/${USERNAME}/.rbenv/plugins/ruby-build fi - ln -s /usr/local/rvm/rubies/default/bin/ruby /usr/local/rvm/gems/default/bin - + # Oryx expects ruby to be installed in this specific path, else it breaks the oryx magic for ruby projects. + if [ ! -f /usr/local/rvm/gems/default/bin/ruby ]; then + ln -s /usr/local/rvm/rubies/default/bin/ruby /usr/local/rvm/gems/default/bin + fi + chown -R "${USERNAME}:rvm" "/home/${USERNAME}/.rbenv/" chmod -R g+r+w "/home/${USERNAME}/.rbenv" find "/home/${USERNAME}/.rbenv" -type d | xargs -n 1 chmod g+s From c58b614202d525b2530d0bbfc45a6a6bbc1a9569 Mon Sep 17 00:00:00 2001 From: Samruddhi Khandale Date: Wed, 16 Aug 2023 11:38:49 -0700 Subject: [PATCH 10/25] Azure-cli: Revert temporary fix which installed az with python (#629) --- src/azure-cli/devcontainer-feature.json | 4 ++-- src/azure-cli/install.sh | 1 - test/azure-cli/install_with_python.sh | 14 ++++++++++++++ test/azure-cli/scenarios.json | 10 ++++++++++ 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 test/azure-cli/install_with_python.sh diff --git a/src/azure-cli/devcontainer-feature.json b/src/azure-cli/devcontainer-feature.json index 09c295c..3d1c0df 100644 --- a/src/azure-cli/devcontainer-feature.json +++ b/src/azure-cli/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "azure-cli", - "version": "1.2.0", + "version": "1.2.1", "name": "Azure CLI", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/azure-cli", "description": "Installs the Azure CLI along with needed dependencies. Useful for base Dockerfiles that often are missing required install dependencies like gpg.", @@ -26,7 +26,7 @@ "installUsingPython": { "type": "boolean", "description": "Install Azure CLI using Python instead of pipx", - "default": true + "default": false } }, "customizations": { diff --git a/src/azure-cli/install.sh b/src/azure-cli/install.sh index 9b0d72d..5175948 100755 --- a/src/azure-cli/install.sh +++ b/src/azure-cli/install.sh @@ -141,7 +141,6 @@ install_using_pip_strategy() { ver="==${AZ_VERSION}" fi - # Temprary quick fix for https://github.com/devcontainers/features/issues/624 if [ "${INSTALL_USING_PYTHON}" = "true" ]; then install_with_complete_python_installation "${ver}" || install_with_pipx "${ver}" || return 1 else diff --git a/test/azure-cli/install_with_python.sh b/test/azure-cli/install_with_python.sh new file mode 100644 index 0000000..4848ac5 --- /dev/null +++ b/test/azure-cli/install_with_python.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -e + +# Import test library for `check` command +source dev-container-features-test-lib + +# Check to make sure the user is vscode +check "user is vscode" whoami | grep vscode +check "version" az --version + + +# Report result +reportResults \ No newline at end of file diff --git a/test/azure-cli/scenarios.json b/test/azure-cli/scenarios.json index 2313452..b7a7323 100644 --- a/test/azure-cli/scenarios.json +++ b/test/azure-cli/scenarios.json @@ -28,5 +28,15 @@ "installBicep": true } } + }, + "install_with_python": { + "image": "mcr.microsoft.com/devcontainers/base:jammy", + "user": "vscode", + "features": { + "azure-cli": { + "version": "latest", + "installUsingPython": true + } + } } } \ No newline at end of file From 9fd5daf06b40794a067c4cdbb8e258d99eeefc24 Mon Sep 17 00:00:00 2001 From: Samruddhi Khandale Date: Wed, 16 Aug 2023 11:43:06 -0700 Subject: [PATCH 11/25] Automated documentation update (#656) Automated documentation update [skip ci] Co-authored-by: github-actions --- src/azure-cli/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-cli/README.md b/src/azure-cli/README.md index c3e23db..2e7b6d3 100644 --- a/src/azure-cli/README.md +++ b/src/azure-cli/README.md @@ -18,7 +18,7 @@ Installs the Azure CLI along with needed dependencies. Useful for base Dockerfil | version | Select or enter an Azure CLI version. (Available versions may vary by Linux distribution.) | string | latest | | extensions | Optional comma separated list of Azure CLI extensions to install in profile. | string | - | | installBicep | Optionally install Azure Bicep | boolean | false | -| installUsingPython | Install Azure CLI using Python instead of pipx | boolean | true | +| installUsingPython | Install Azure CLI using Python instead of pipx | boolean | false | ## Customizations From a4b31f314fa1385950a9dbc81682d9acb2e2fba9 Mon Sep 17 00:00:00 2001 From: Samruddhi Khandale Date: Fri, 18 Aug 2023 16:01:50 -0700 Subject: [PATCH 12/25] [docker-in-docker] Reverts the retry logic (#659) * Update install.sh * [docker-in-docker] Update retry logic * revert the retry logic! --- .../devcontainer-feature.json | 2 +- src/docker-in-docker/install.sh | 30 ++++------------- test/docker-in-docker/docker_retry.sh | 32 ------------------- test/docker-in-docker/scenarios.json | 6 ---- .../test-scripts/docker-test-init.sh | 26 --------------- 5 files changed, 7 insertions(+), 89 deletions(-) delete mode 100644 test/docker-in-docker/docker_retry.sh delete mode 100644 test/docker-in-docker/test-scripts/docker-test-init.sh diff --git a/src/docker-in-docker/devcontainer-feature.json b/src/docker-in-docker/devcontainer-feature.json index b5e0aee..2afdd11 100644 --- a/src/docker-in-docker/devcontainer-feature.json +++ b/src/docker-in-docker/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "docker-in-docker", - "version": "2.3.0", + "version": "2.3.1", "name": "Docker (Docker-in-Docker)", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/docker-in-docker", "description": "Create child containers *inside* a container, independent from the host's docker instance. Installs Docker extension in the container along with needed CLIs.", diff --git a/src/docker-in-docker/install.sh b/src/docker-in-docker/install.sh index f7e5a7d..bdef126 100755 --- a/src/docker-in-docker/install.sh +++ b/src/docker-in-docker/install.sh @@ -423,30 +423,12 @@ dockerd_start="AZURE_DNS_AUTO_DETECTION=${AZURE_DNS_AUTO_DETECTION} DOCKER_DEFAU INNEREOF )" -retry_count=0 -docker_ok="false" - -until [ "${docker_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; -do - # 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 - docker info > /dev/null 2>&1 && docker_ok="true" - - if [ "${docker_ok}" != "true" ]; then - echo "(*) Failed to start docker, retrying in 5s..." - retry_count=`expr $retry_count + 1` - sleep 5s - fi - set -e -done - -set +e +# 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 # Execute whatever commands were passed in (if any). This allows us # to set this script to ENTRYPOINT while still executing the default CMD. diff --git a/test/docker-in-docker/docker_retry.sh b/test/docker-in-docker/docker_retry.sh deleted file mode 100644 index cc35fbb..0000000 --- a/test/docker-in-docker/docker_retry.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -set -e - -# Optional: Import test library -source dev-container-features-test-lib - -# Definition specific tests -check "docker-buildx" docker buildx version -check "docker-ps" docker ps - -sleep 5s - -# Stop docker -pkill dockerd -pkill containerd - -sleep 5s - -set +e - docker_ok_code="$(docker info > /dev/null 2>&1; echo $?)" -set -e - -check "docker-not-running" bash -c "[[ ${docker_ok_code} == 1 ]]" - -# Testing retry logic -./test-scripts/docker-test-init.sh - -check "docker-started-after-retries" docker ps - -# Report result -reportResults diff --git a/test/docker-in-docker/scenarios.json b/test/docker-in-docker/scenarios.json index 525ce69..de4e7d6 100644 --- a/test/docker-in-docker/scenarios.json +++ b/test/docker-in-docker/scenarios.json @@ -67,11 +67,5 @@ } }, "remoteUser": "node" - }, - "docker_retry": { - "image": "ubuntu:focal", - "features": { - "docker-in-docker": {} - } } } diff --git a/test/docker-in-docker/test-scripts/docker-test-init.sh b/test/docker-in-docker/test-scripts/docker-test-init.sh deleted file mode 100644 index b213fb4..0000000 --- a/test/docker-in-docker/test-scripts/docker-test-init.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/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. -#------------------------------------------------------------------------------------------------------------- - -retry_count=0 -docker_ok="false" - -until [ "${docker_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; -do - if [ "${retry_count}" -eq "3" ]; then - echo "Starting docker after 3 retries..." - /usr/local/share/docker-init.sh - fi - - set +e - docker info > /dev/null 2>&1 && docker_ok="true" - - if [ "${docker_ok}" != "true" ]; then - echo "(*) Failed to start docker, retrying in 5s... Retry count: ${retry_count}" - retry_count=`expr $retry_count + 1` - sleep 1s - fi - set -e -done \ No newline at end of file From 71ac2021e1350e673c0602b2ab6526d4353bc293 Mon Sep 17 00:00:00 2001 From: Bulat Khusnimardanov <61210700+bulat-khusnimardanov@users.noreply.github.com> Date: Tue, 22 Aug 2023 02:31:20 +0200 Subject: [PATCH 13/25] Enhancing Terraform Feature Stability (#657) * Added fallback method to retrieve cosign version * Update src/terraform/install.sh Co-authored-by: Samruddhi Khandale * Remove get_cosign_latest_version function --------- Co-authored-by: Samruddhi Khandale --- src/terraform/devcontainer-feature.json | 2 +- src/terraform/install.sh | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/terraform/devcontainer-feature.json b/src/terraform/devcontainer-feature.json index b1e6df2..119b433 100644 --- a/src/terraform/devcontainer-feature.json +++ b/src/terraform/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "terraform", - "version": "1.3.3", + "version": "1.3.4", "name": "Terraform, tflint, and TFGrunt", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/terraform", "description": "Installs the Terraform CLI and optionally TFLint and Terragrunt. Auto-detects latest version and installs needed dependencies.", diff --git a/src/terraform/install.sh b/src/terraform/install.sh index 82e3e8d..1156f6d 100755 --- a/src/terraform/install.sh +++ b/src/terraform/install.sh @@ -165,7 +165,8 @@ ensure_cosign() { if ! type cosign > /dev/null 2>&1; then echo "Installing cosign..." - local LATEST_COSIGN_VERSION=$(curl https://api.github.com/repos/sigstore/cosign/releases/latest | grep tag_name | cut -d : -f2 | tr -d "v\", ") + LATEST_COSIGN_VERSION="latest" + find_version_from_git_tags LATEST_COSIGN_VERSION 'https://github.com/sigstore/cosign' curl -L "https://github.com/sigstore/cosign/releases/latest/download/cosign_${LATEST_COSIGN_VERSION}_${architecture}.deb" -o /tmp/cosign_${LATEST_COSIGN_VERSION}_${architecture}.deb dpkg -i /tmp/cosign_${LATEST_COSIGN_VERSION}_${architecture}.deb From a08434f2b18627f31107a4ef6ab4f578a92aeaec Mon Sep 17 00:00:00 2001 From: nyuyuyu <127820811+nyuyuyu@users.noreply.github.com> Date: Fri, 25 Aug 2023 01:38:40 +0900 Subject: [PATCH 14/25] [Rust] Remove `mutantdino.resourcemonitor` extension (#665) * Remove `mutantdino.resourcemonitor` extension * Bump version * Revert "Bump version" This reverts commit e9c514911dd6de6b03e235536e8602f62cbe5b99. * Bump version --- src/rust/devcontainer-feature.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/rust/devcontainer-feature.json b/src/rust/devcontainer-feature.json index bc9044c..70ed39e 100644 --- a/src/rust/devcontainer-feature.json +++ b/src/rust/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "rust", - "version": "1.0.12", + "version": "1.1.0", "name": "Rust", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/rust", "description": "Installs Rust, common Rust utilities, and their required dependencies", @@ -39,7 +39,6 @@ "vscode": { "extensions": [ "vadimcn.vscode-lldb", - "mutantdino.resourcemonitor", "rust-lang.rust-analyzer", "tamasfe.even-better-toml", "serayuzgur.crates" From 0cf690ee14279d9e7ae5e1308950d6067d3aba81 Mon Sep 17 00:00:00 2001 From: Samruddhi Khandale Date: Thu, 24 Aug 2023 12:31:40 -0700 Subject: [PATCH 15/25] [docker in docker] Adds retries for docker daemon startup and cgroup nesting (#669) * adds retry logic * adds stress test workflow * Adds cgroup retries * update workflow * move sleep cmd * update test * cleanup --- .../docker-in-docker-stress-test.yaml | 37 +++++++ .../devcontainer-feature.json | 2 +- src/docker-in-docker/install.sh | 102 +++++++++++++----- .../docker_with_on_create_command.sh | 18 ++++ test/docker-in-docker/scenarios.json | 9 ++ test/docker-in-docker/test.sh | 2 +- 6 files changed, 139 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/docker-in-docker-stress-test.yaml create mode 100755 test/docker-in-docker/docker_with_on_create_command.sh diff --git a/.github/workflows/docker-in-docker-stress-test.yaml b/.github/workflows/docker-in-docker-stress-test.yaml new file mode 100644 index 0000000..569f946 --- /dev/null +++ b/.github/workflows/docker-in-docker-stress-test.yaml @@ -0,0 +1,37 @@ +name: "Stress test - Docker in Docker" +on: + pull_request: + paths: + - 'src/docker-in-docker/**' + workflow_dispatch: + +jobs: + test: + strategy: + matrix: + test-pass: [ 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50 ] + fail-fast: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: "Install latest devcontainer CLI" + run: npm install -g @devcontainers/cli + + - name: "Generating tests for 'docker-in-docker' which validates if docker daemon is running" + run: devcontainer features test --skip-scenarios -f docker-in-docker -i mcr.microsoft.com/devcontainers/base:ubuntu . + + test-onCreate: + strategy: + matrix: + test-pass: [ 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50 ] + fail-fast: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: "Install latest devcontainer CLI" + run: npm install -g @devcontainers/cli + + - name: "Generating tests for 'docker-in-docker' which validates if docker daemon is available within 'onCreateCommand'" + run: devcontainer features test -f docker-in-docker --skip-autogenerated --filter "docker_with_on_create_command" \ No newline at end of file diff --git a/src/docker-in-docker/devcontainer-feature.json b/src/docker-in-docker/devcontainer-feature.json index 2afdd11..edfaa1f 100644 --- a/src/docker-in-docker/devcontainer-feature.json +++ b/src/docker-in-docker/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "docker-in-docker", - "version": "2.3.1", + "version": "2.4.0", "name": "Docker (Docker-in-Docker)", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/docker-in-docker", "description": "Create child containers *inside* a container, independent from the host's docker instance. Installs Docker extension in the container along with needed CLIs.", diff --git a/src/docker-in-docker/install.sh b/src/docker-in-docker/install.sh index bdef126..a0d4a94 100755 --- a/src/docker-in-docker/install.sh +++ b/src/docker-in-docker/install.sh @@ -367,7 +367,7 @@ dockerd_start="AZURE_DNS_AUTO_DETECTION=${AZURE_DNS_AUTO_DETECTION} DOCKER_DEFAU 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 + # -- Start: dind wrapper script -- # Maintained: https://github.com/moby/moby/blob/master/hack/dind export container=docker @@ -384,31 +384,52 @@ dockerd_start="AZURE_DNS_AUTO_DETECTION=${AZURE_DNS_AUTO_DETECTION} DOCKER_DEFAU 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. + set_cgroup_nesting() + { + # 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 + } + + # Set cgroup nesting, retrying if necessary + retry_cgroup_nesting=0 + + until [ "${retry_cgroup_nesting}" -eq "5" ]; + do + set +e + set_cgroup_nesting + + if [ $? -ne 0 ]; then + echo "(*) cgroup v2: Failed to enable nesting, retrying..." + else + break + fi + + retry_cgroup_nesting=`expr $retry_cgroup_nesting + 1` + set -e + done + + # -- End: dind wrapper script -- # Handle DNS set +e - cat /etc/resolv.conf | grep -i 'internal.cloudapp.net' - if [ $? -eq 0 ] && [ "${AZURE_DNS_AUTO_DETECTION}" = "true" ] - then - echo "Setting dockerd Azure DNS." - CUSTOMDNS="--dns 168.63.129.16" - else - echo "Not setting dockerd DNS manually." - CUSTOMDNS="" - fi - + cat /etc/resolv.conf | grep -i 'internal.cloudapp.net' > /dev/null 2>&1 + if [ $? -eq 0 ] && [ "${AZURE_DNS_AUTO_DETECTION}" = "true" ] + then + echo "Setting dockerd Azure DNS." + CUSTOMDNS="--dns 168.63.129.16" + else + echo "Not setting dockerd DNS manually." + CUSTOMDNS="" + fi set -e if [ -z "$DOCKER_DEFAULT_ADDRESS_POOL" ] @@ -423,12 +444,35 @@ dockerd_start="AZURE_DNS_AUTO_DETECTION=${AZURE_DNS_AUTO_DETECTION} DOCKER_DEFAU 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 +retry_docker_start_count=0 +docker_ok="false" + +until [ "${docker_ok}" = "true" ] || [ "${retry_docker_start_count}" -eq "5" ]; +do + # 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 + + retry_count=0 + until [ "${docker_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; + do + sleep 1s + set +e + docker info > /dev/null 2>&1 && docker_ok="true" + set -e + + retry_count=`expr $retry_count + 1` + done + + if [ "${docker_ok}" != "true" ]; then + echo "(*) Failed to start docker, retrying..." + fi + + retry_docker_start_count=`expr $retry_docker_start_count + 1` +done # Execute whatever commands were passed in (if any). This allows us # to set this script to ENTRYPOINT while still executing the default CMD. diff --git a/test/docker-in-docker/docker_with_on_create_command.sh b/test/docker-in-docker/docker_with_on_create_command.sh new file mode 100755 index 0000000..131304a --- /dev/null +++ b/test/docker-in-docker/docker_with_on_create_command.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Feature specific tests +check "version" docker --version +check "docker-ps" bash -c "docker ps" + +check "docker-init-exists" bash -c "ls /usr/local/share/docker-init.sh" +check "log-exists" bash -c "ls /tmp/dockerd.log" +check "log-for-completion" bash -c "cat /tmp/dockerd.log | grep 'Daemon has completed initialization'" +check "log-contents" bash -c "cat /tmp/dockerd.log | grep 'API listen on /var/run/docker.sock'" + +# Report result +reportResults \ No newline at end of file diff --git a/test/docker-in-docker/scenarios.json b/test/docker-in-docker/scenarios.json index de4e7d6..1274877 100644 --- a/test/docker-in-docker/scenarios.json +++ b/test/docker-in-docker/scenarios.json @@ -67,5 +67,14 @@ } }, "remoteUser": "node" + }, + // DO NOT REMOVE: This scenario is used by the docker-in-docker-stress-test workflow + "docker_with_on_create_command": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "docker-in-docker": {} + }, + "remoteUser": "vscode", + "onCreateCommand": "docker ps && sleep 5s && docker ps" } } diff --git a/test/docker-in-docker/test.sh b/test/docker-in-docker/test.sh index 1eebfd2..e86a841 100755 --- a/test/docker-in-docker/test.sh +++ b/test/docker-in-docker/test.sh @@ -8,10 +8,10 @@ source dev-container-features-test-lib # Feature specific tests check "version" docker --version check "docker-init-exists" bash -c "ls /usr/local/share/docker-init.sh" +check "docker-ps" bash -c "docker ps" check "log-exists" bash -c "ls /tmp/dockerd.log" check "log-for-completion" bash -c "cat /tmp/dockerd.log | grep 'Daemon has completed initialization'" check "log-contents" bash -c "cat /tmp/dockerd.log | grep 'API listen on /var/run/docker.sock'" -check "docker-ps" bash -c "docker ps" # Report result reportResults \ No newline at end of file From 0668db5edea56a0d57575db127b793f59b489e7b Mon Sep 17 00:00:00 2001 From: naturedamends <120284608+naturedamends@users.noreply.github.com> Date: Sat, 26 Aug 2023 00:39:32 +0100 Subject: [PATCH 16/25] Commonutils: chown: cannot access '/home/vscode/.zshrc': No such file or directory (#662) * Update main.sh * Update devcontainer-feature.json * Test install as non root user. --------- Co-authored-by: Samruddhi Khandale --- src/common-utils/devcontainer-feature.json | 2 +- src/common-utils/main.sh | 2 +- .../configure_zsh_no_template_first_step.sh | 12 ++++++++++++ ... => configure_zsh_no_template_second_step.sh} | 5 ++--- test/common-utils/scenarios.json | 16 +++++++++++++--- 5 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 test/common-utils/configure_zsh_no_template_first_step.sh rename test/common-utils/{configure_zsh_no_template.sh => configure_zsh_no_template_second_step.sh} (57%) diff --git a/src/common-utils/devcontainer-feature.json b/src/common-utils/devcontainer-feature.json index 4eaef21..747be30 100644 --- a/src/common-utils/devcontainer-feature.json +++ b/src/common-utils/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "common-utils", - "version": "2.1.1", + "version": "2.1.2", "name": "Common Utilities", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/common-utils", "description": "Installs a set of common command line utilities, Oh My Zsh!, and sets up a non-root user.", diff --git a/src/common-utils/main.sh b/src/common-utils/main.sh index 3731994..902f9e6 100644 --- a/src/common-utils/main.sh +++ b/src/common-utils/main.sh @@ -500,7 +500,7 @@ if [ "${INSTALL_ZSH}" = "true" ]; then copy_to_user_files=("${oh_my_install_dir}") [ -f "$user_rc_file" ] && copy_to_user_files+=("$user_rc_file") cp -rf "${copy_to_user_files[@]}" /root - chown -R ${USERNAME}:${group_name} "${oh_my_install_dir}" "${user_rc_file}" + chown -R ${USERNAME}:${group_name} "${copy_to_user_files[@]}" fi fi fi diff --git a/test/common-utils/configure_zsh_no_template_first_step.sh b/test/common-utils/configure_zsh_no_template_first_step.sh new file mode 100644 index 0000000..d26874d --- /dev/null +++ b/test/common-utils/configure_zsh_no_template_first_step.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Definition specific tests +check "default-zsh-with-no-zshrc" bash -c "[ ! -e ~/.zshrc ]" + +# Report result +reportResults diff --git a/test/common-utils/configure_zsh_no_template.sh b/test/common-utils/configure_zsh_no_template_second_step.sh similarity index 57% rename from test/common-utils/configure_zsh_no_template.sh rename to test/common-utils/configure_zsh_no_template_second_step.sh index c62cae9..734fa66 100644 --- a/test/common-utils/configure_zsh_no_template.sh +++ b/test/common-utils/configure_zsh_no_template_second_step.sh @@ -4,12 +4,11 @@ set -e # Optional: Import test library source dev-container-features-test-lib - # Definition specific tests function file_not_overridden() { - cat ~/.zshrc | grep 'alias fnomockalias=' | grep testingmock + cat $1 | grep 'alias fnomockalias=' | grep testingmock } -check "default-zsh-with-no-zshrc" file_not_overridden +check "default-zsh-with-no-zshrc" file_not_overridden /home/devcontainer/.zshrc # Report result reportResults diff --git a/test/common-utils/scenarios.json b/test/common-utils/scenarios.json index 904a47b..d7b296d 100644 --- a/test/common-utils/scenarios.json +++ b/test/common-utils/scenarios.json @@ -115,10 +115,20 @@ } } }, - "configure_zsh_no_template": { + "configure_zsh_no_template_second_step": { "image": "mcr.microsoft.com/devcontainers/base:ubuntu", - "postCreateCommand": "echo alias fnomockalias=testingmock >> /root/.zshrc", - "remoteUser": "root", + "postCreateCommand": "echo alias fnomockalias=testingmock >> /home/devcontainer/.zshrc", + "remoteUser": "devcontainer", + "features": { + "common-utils": { + "installZsh": true, + "installOhMyZshConfig": false + } + } + }, + "configure_zsh_no_template_first_step": { + "image": "debian:bullseye", + "remoteUser": "devcontainer", "features": { "common-utils": { "installZsh": true, From 95a169b9ef92c9dae454df5e87d4c26d56a863c9 Mon Sep 17 00:00:00 2001 From: eitsupi <50911393+eitsupi@users.noreply.github.com> Date: Tue, 29 Aug 2023 06:26:30 +0900 Subject: [PATCH 17/25] [node] Install the latest version of nvm by default (#673) feat(node): install the latest nvm by default --- src/node/devcontainer-feature.json | 10 ++++--- src/node/install.sh | 42 +++++++++++++++++++++++++++++- test/node/install_nvm_0.39.sh | 14 ++++++++++ test/node/scenarios.json | 10 ++++++- 4 files changed, 71 insertions(+), 5 deletions(-) create mode 100755 test/node/install_nvm_0.39.sh diff --git a/src/node/devcontainer-feature.json b/src/node/devcontainer-feature.json index 80bd912..aee55a1 100644 --- a/src/node/devcontainer-feature.json +++ b/src/node/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "node", - "version": "1.2.1", + "version": "1.3.0", "name": "Node.js (via nvm), yarn and pnpm", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/node", "description": "Installs Node.js, nvm, yarn, pnpm, and needed dependencies.", @@ -30,7 +30,11 @@ }, "nvmVersion": { "type": "string", - "default": "0.39.2", + "proposals": [ + "latest", + "0.39" + ], + "default": "latest", "description": "Version of NVM to install." } }, @@ -49,4 +53,4 @@ "installsAfter": [ "ghcr.io/devcontainers/features/common-utils" ] -} \ No newline at end of file +} diff --git a/src/node/install.sh b/src/node/install.sh index 1ce8fee..8091094 100755 --- a/src/node/install.sh +++ b/src/node/install.sh @@ -8,7 +8,7 @@ # Maintainer: The Dev Container spec maintainers export NODE_VERSION="${VERSION:-"lts"}" -export NVM_VERSION="${NVMVERSION:-"0.39.2"}" +export NVM_VERSION="${NVMVERSION:-"latest"}" export NVM_DIR="${NVMINSTALLPATH:-"/usr/local/share/nvm"}" INSTALL_TOOLS_FOR_NODE_GYP="${NODEGYPDEPENDENCIES:-true}" @@ -78,6 +78,40 @@ check_packages() { 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 @@ -92,6 +126,10 @@ fi # Install dependencies check_packages apt-transport-https curl ca-certificates tar gnupg2 dirmngr +if ! type git > /dev/null 2>&1; then + check_packages git +fi + # Install yarn if type yarn > /dev/null 2>&1; then echo "Yarn already installed." @@ -112,6 +150,8 @@ elif [ "${NODE_VERSION}" = "latest" ]; then export NODE_VERSION="node" fi +find_version_from_git_tags NVM_VERSION "https://github.com/nvm-sh/nvm" + # Install snipppet that we will run as the user nvm_install_snippet="$(cat << EOF set -e diff --git a/test/node/install_nvm_0.39.sh b/test/node/install_nvm_0.39.sh new file mode 100755 index 0000000..6b96381 --- /dev/null +++ b/test/node/install_nvm_0.39.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Definition specific tests +check "version" bash -c "node --version" +check "pnpm" pnpm -v +check "nvm version" bash -c ". /usr/local/share/nvm/nvm.sh && nvm --version | grep 0.39" + +# Report result +reportResults diff --git a/test/node/scenarios.json b/test/node/scenarios.json index 5524f5c..a45620f 100644 --- a/test/node/scenarios.json +++ b/test/node/scenarios.json @@ -44,5 +44,13 @@ "version": "16" } } + }, + "install_nvm_0.39": { + "image": "mcr.microsoft.com/devcontainers/base", + "features": { + "node": { + "nvmVersion": "0.39" + } + } } -} \ No newline at end of file +} From 96bff0097028001e6e4126c5528d37cb8c13e785 Mon Sep 17 00:00:00 2001 From: Samruddhi Khandale Date: Mon, 28 Aug 2023 15:15:43 -0700 Subject: [PATCH 18/25] Automated documentation update (#676) Automated documentation update [skip ci] Co-authored-by: github-actions --- src/node/README.md | 2 +- src/rust/README.md | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/node/README.md b/src/node/README.md index 7a49096..b983919 100644 --- a/src/node/README.md +++ b/src/node/README.md @@ -18,7 +18,7 @@ Installs Node.js, nvm, yarn, pnpm, and needed dependencies. | version | Select or enter a Node.js version to install | string | lts | | nodeGypDependencies | Install dependencies to compile native node modules (node-gyp)? | boolean | true | | nvmInstallPath | The path where NVM will be installed. | string | /usr/local/share/nvm | -| nvmVersion | Version of NVM to install. | string | 0.39.2 | +| nvmVersion | Version of NVM to install. | string | latest | ## Customizations diff --git a/src/rust/README.md b/src/rust/README.md index f72b56f..974d785 100644 --- a/src/rust/README.md +++ b/src/rust/README.md @@ -23,7 +23,6 @@ Installs Rust, common Rust utilities, and their required dependencies ### VS Code Extensions - `vadimcn.vscode-lldb` -- `mutantdino.resourcemonitor` - `rust-lang.rust-analyzer` - `tamasfe.even-better-toml` - `serayuzgur.crates` From 038bed3d58a84885da8a008b80905da17d57a543 Mon Sep 17 00:00:00 2001 From: eitsupi <50911393+eitsupi@users.noreply.github.com> Date: Wed, 30 Aug 2023 01:32:46 +0900 Subject: [PATCH 19/25] ci: remove `ubuntu:bionic` from test matrixes (#677) ci: remove ubuntu:bionic from test matrixes --- .github/workflows/test-all.yaml | 3 +-- .github/workflows/test-pr.yaml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-all.yaml b/.github/workflows/test-all.yaml index 8dc206d..40f5efa 100644 --- a/.github/workflows/test-all.yaml +++ b/.github/workflows/test-all.yaml @@ -42,7 +42,6 @@ jobs: ] baseImage: [ - "ubuntu:bionic", "ubuntu:focal", "ubuntu:jammy", "debian:11", @@ -112,4 +111,4 @@ jobs: run: npm install -g @devcontainers/cli - name: "Testing global scenarios" - run: devcontainer features test --global-scenarios-only . \ No newline at end of file + run: devcontainer features test --global-scenarios-only . diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index 6b91b61..d607335 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -49,7 +49,6 @@ jobs: features: ${{ fromJSON(needs.detect-changes.outputs.features) }} baseImage: [ - "ubuntu:bionic", "ubuntu:focal", "ubuntu:jammy", "debian:11", @@ -80,4 +79,4 @@ jobs: run: npm install -g @devcontainers/cli - name: "Testing '${{ matrix.features }}' scenarios" - run: devcontainer features test -f ${{ matrix.features }} --skip-autogenerated . \ No newline at end of file + run: devcontainer features test -f ${{ matrix.features }} --skip-autogenerated . From 96c1eea40fc97b471ba0b33fcc79273c7ce586c7 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 11 Sep 2023 20:16:24 +0200 Subject: [PATCH 20/25] Use dotnet-install.sh in .NET feature (#628) * Use dotnet-install.sh in .NET feature * Use latest.version files * Cleanup runtime args * Use latest.version files in tests as well * Improve tests, remove code duplication * Add stderr helper * Validate version inputs * Use suggested description Co-authored-by: Samruddhi Khandale * Shorter version description Co-authored-by: Samruddhi Khandale * Shorter version description Co-authored-by: Samruddhi Khandale * Clean up apt lists * Verify 7.0 is latest * Fix PATH, add test for .NET global tools * Include a copy of dotnet-install.sh in the Feature * Configure useful env variables * Use stringly typed booleans * Keep imperative writing style in option hints * Update maintainers Co-authored-by: Samruddhi Khandale * Move dotnet-install.sh into a vendor directory * Refactor variables * Amend * Amend 2 * Use default options from devcontainer-feature.json * Add back variables * Fix shellchek warning in fetch_latest_sdk_version * Inline install_version function * Fix ShellCheck warnings * Improve CSV parsing * Default to latest when configuring an empty version * Add support for runtime-only configurations * Move 'none' check higher up * Deduplicate helper functions, sort into files * Address the user more directly in NOTES * Remove unnecessary defaults * Replace feature -> Feature Co-authored-by: Samruddhi Khandale * Add update-dotnet-install-script workflow * Apply suggestions from code review Co-authored-by: Samruddhi Khandale * Don't skip ci for automated script update --------- Co-authored-by: Samruddhi Khandale --- .../update-dotnet-install-script.yml | 48 + src/dotnet/NOTES.md | 67 + src/dotnet/devcontainer-feature.json | 41 +- src/dotnet/install.sh | 516 +---- src/dotnet/scripts/dotnet-helpers.sh | 119 ++ .../scripts/fetch-latest-dotnet-install.sh | 16 + src/dotnet/scripts/string-helpers.sh | 42 + src/dotnet/scripts/vendor/README.md | 27 + src/dotnet/scripts/vendor/dotnet-install.sh | 1746 +++++++++++++++++ test/dotnet/dotnet_env.sh | 4 + test/dotnet/dotnet_helpers.sh | 71 + test/dotnet/install_additional_dotnet.sh | 17 - .../dotnet/install_aspnetcore_runtime_only.sh | 27 + test/dotnet/install_dotnet_3.sh | 17 - test/dotnet/install_dotnet_6_bullseye.sh | 17 - test/dotnet/install_dotnet_6_focal.sh | 17 - test/dotnet/install_dotnet_6_jammy.sh | 17 - test/dotnet/install_dotnet_7_bullseye.sh | 17 - test/dotnet/install_dotnet_7_jammy.sh | 17 - test/dotnet/install_dotnet_exact_version.sh | 24 + test/dotnet/install_dotnet_global_tool.sh | 25 + test/dotnet/install_dotnet_latest.sh | 17 - ...all_dotnet_latest_when_version_is_empty.sh | 30 + test/dotnet/install_dotnet_lts.sh | 27 +- .../install_dotnet_multiple_versions.sh | 30 + test/dotnet/install_dotnet_runtime_only.sh | 27 + .../dotnet/install_dotnet_specific_release.sh | 26 + ...otnet_specific_release_and_feature_band.sh | 24 + test/dotnet/install_wo_apt.sh | 17 - test/dotnet/projects/.gitignore | 2 + test/dotnet/projects/multitargeting/Class1.cs | 4 + .../multitargeting/example_classlib.csproj | 9 + test/dotnet/projects/net5.0/Program.cs | 32 + .../projects/net5.0/example_project.csproj | 13 + test/dotnet/projects/net6.0/Program.cs | 30 + .../projects/net6.0/example_project.csproj | 14 + test/dotnet/projects/net7.0/Program.cs | 30 + .../projects/net7.0/example_project.csproj | 14 + test/dotnet/projects/net8.0/Program.cs | 32 + .../projects/net8.0/example_project.csproj | 14 + test/dotnet/projects/netcoreapp3.1/Program.cs | 39 + .../netcoreapp3.1/example_project.csproj | 14 + test/dotnet/scenarios.json | 141 +- test/dotnet/test.sh | 31 +- 44 files changed, 2805 insertions(+), 704 deletions(-) create mode 100644 .github/workflows/update-dotnet-install-script.yml create mode 100644 src/dotnet/scripts/dotnet-helpers.sh create mode 100755 src/dotnet/scripts/fetch-latest-dotnet-install.sh create mode 100644 src/dotnet/scripts/string-helpers.sh create mode 100644 src/dotnet/scripts/vendor/README.md create mode 100755 src/dotnet/scripts/vendor/dotnet-install.sh create mode 100644 test/dotnet/dotnet_env.sh create mode 100644 test/dotnet/dotnet_helpers.sh delete mode 100644 test/dotnet/install_additional_dotnet.sh create mode 100644 test/dotnet/install_aspnetcore_runtime_only.sh delete mode 100644 test/dotnet/install_dotnet_3.sh delete mode 100644 test/dotnet/install_dotnet_6_bullseye.sh delete mode 100644 test/dotnet/install_dotnet_6_focal.sh delete mode 100644 test/dotnet/install_dotnet_6_jammy.sh delete mode 100644 test/dotnet/install_dotnet_7_bullseye.sh delete mode 100644 test/dotnet/install_dotnet_7_jammy.sh create mode 100644 test/dotnet/install_dotnet_exact_version.sh create mode 100644 test/dotnet/install_dotnet_global_tool.sh delete mode 100644 test/dotnet/install_dotnet_latest.sh create mode 100644 test/dotnet/install_dotnet_latest_when_version_is_empty.sh create mode 100644 test/dotnet/install_dotnet_multiple_versions.sh create mode 100644 test/dotnet/install_dotnet_runtime_only.sh create mode 100644 test/dotnet/install_dotnet_specific_release.sh create mode 100644 test/dotnet/install_dotnet_specific_release_and_feature_band.sh delete mode 100644 test/dotnet/install_wo_apt.sh create mode 100644 test/dotnet/projects/.gitignore create mode 100644 test/dotnet/projects/multitargeting/Class1.cs create mode 100644 test/dotnet/projects/multitargeting/example_classlib.csproj create mode 100644 test/dotnet/projects/net5.0/Program.cs create mode 100644 test/dotnet/projects/net5.0/example_project.csproj create mode 100644 test/dotnet/projects/net6.0/Program.cs create mode 100644 test/dotnet/projects/net6.0/example_project.csproj create mode 100644 test/dotnet/projects/net7.0/Program.cs create mode 100644 test/dotnet/projects/net7.0/example_project.csproj create mode 100644 test/dotnet/projects/net8.0/Program.cs create mode 100644 test/dotnet/projects/net8.0/example_project.csproj create mode 100644 test/dotnet/projects/netcoreapp3.1/Program.cs create mode 100644 test/dotnet/projects/netcoreapp3.1/example_project.csproj mode change 100755 => 100644 test/dotnet/test.sh diff --git a/.github/workflows/update-dotnet-install-script.yml b/.github/workflows/update-dotnet-install-script.yml new file mode 100644 index 0000000..56adf85 --- /dev/null +++ b/.github/workflows/update-dotnet-install-script.yml @@ -0,0 +1,48 @@ +name: "Updates vendor 'dotnet-install' script" +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * 0' # Runs every Sunday at midnight UTC (adjust as needed) + +jobs: + fetch-latest-dotnet-install: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Run fetch-latest-dotnet-install.sh + run: src/dotnet/scripts/fetch-latest-dotnet-install.sh + + - name: Create a PR for dotnet-install.sh + id: push_image_info + env: + GITHUB_TOKEN: ${{ secrets.PAT }} + run: | + set -e + echo "Start." + + # Configure git and Push updates + git config --global user.email github-actions@github.com + git config --global user.name github-actions + git config pull.rebase false + + branch=automated-script-update-$GITHUB_RUN_ID + git checkout -b $branch + message='[Updates] Automated vendor 'dotnet-install' script' + + # Add / update and commit + git add src/dotnet/scripts/vendor/dotnet-install.sh + git commit -m 'Automated dotnet-install script update' || export NO_UPDATES=true + + # Push + if [ "$NO_UPDATES" != "true" ] ; then + git push origin "$branch" + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + /repos/${GITHUB_REPOSITORY}/pulls \ + -f title="$message" \ + -f body="$message" \ + -f head="$branch" \ + -f base="$GITHUB_REF_NAME" + fi diff --git a/src/dotnet/NOTES.md b/src/dotnet/NOTES.md index 19fe92f..578acea 100644 --- a/src/dotnet/NOTES.md +++ b/src/dotnet/NOTES.md @@ -1,4 +1,71 @@ +## Configuration examples +Installing only the latest .NET SDK version (the default). + +``` json +{ +"features": { + "ghcr.io/devcontainers/features/dotnet:2": "latest" // or "" or {} +} +``` + +Installing an additional SDK version. Multiple versions can be specified as comma-separated values. + +``` json +{ +"features": { + "ghcr.io/devcontainers/features/dotnet:2": { + "additionalVersions": "lts" + } +} +``` + +Installing specific SDK versions. + +``` json +{ +"features": { + "ghcr.io/devcontainers/features/dotnet:2": { + "version": "6.0", + "additionalVersions": "7.0, 8.0" + } +} +``` + +Installing a specific SDK feature band. + +``` json +{ +"features": { + "ghcr.io/devcontainers/features/dotnet:2": { + "version": "6.0.4xx", + } +} +``` + +Installing a specific SDK patch version. + +``` json +{ +"features": { + "ghcr.io/devcontainers/features/dotnet:2": { + "version": "6.0.412", + } +} +``` + +Installing only the .NET Runtime or the ASP.NET Core Runtime. (The SDK includes all runtimes so this configuration is only useful if you need to run .NET apps without building them from source.) + +``` json +{ +"features": { + "ghcr.io/devcontainers/features/dotnet:2": { + "version": "none", + "dotnetRuntimeVersions": "latest, lts", + "aspnetCoreRuntimeVersions": "latest, lts", + } +} +``` ## OS Support diff --git a/src/dotnet/devcontainer-feature.json b/src/dotnet/devcontainer-feature.json index 4f484cc..f00e0d3 100644 --- a/src/dotnet/devcontainer-feature.json +++ b/src/dotnet/devcontainer-feature.json @@ -1,35 +1,44 @@ { "id": "dotnet", - "version": "1.1.4", + "version": "2.0.0", "name": "Dotnet CLI", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/dotnet", - "description": "Installs the .NET CLI. Provides option of installing sdk or runtime, and option of versions to install. Uses latest version of .NET sdk as defaults to install.", + "description": "This Feature installs the latest .NET SDK, which includes the .NET CLI and the shared runtime. Options are provided to choose a different version or additional versions.", "options": { "version": { "type": "string", "proposals": [ "latest", - "7", - "6", - "3.1" + "lts", + "none", + "8.0", + "7.0", + "6.0" ], "default": "latest", - "description": "Select or enter a dotnet CLI version. (Available versions may vary by Linux distribution.)" + "description": "Select or enter a .NET SDK version. Use 'latest' for the latest version, 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version." }, - "runtimeOnly": { - "type": "boolean", - "default": false, - "description": "Install just the dotnet runtime if true, and sdk if false." + "additionalVersions": { + "type": "string", + "default": "", + "description": "Enter additional .NET SDK versions, separated by commas. Use 'latest' for the latest version, 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version." }, - "installUsingApt": { - "type": "boolean", - "default": true, - "description": "If true, it installs using apt instead of the release URL" + "dotnetRuntimeVersions": { + "type": "string", + "default": "", + "description": "Enter additional .NET runtime versions, separated by commas. Use 'latest' for the latest version, 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version." + }, + "aspNetCoreRuntimeVersions": { + "type": "string", + "default": "", + "description": "Enter additional ASP.NET Core runtime versions, separated by commas. Use 'latest' for the latest version, 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version." } }, "containerEnv": { - "DOTNET_ROOT": "/usr/local/dotnet/current", - "PATH": "/usr/local/dotnet/current:${PATH}" + "DOTNET_ROOT": "/usr/share/dotnet", + "PATH": "$PATH:$DOTNET_ROOT:~/.dotnet/tools", + "DOTNET_RUNNING_IN_CONTAINER": "true", + "DOTNET_USE_POLLING_FILE_WATCHER": "true" }, "customizations": { "vscode": { diff --git a/src/dotnet/install.sh b/src/dotnet/install.sh index 178994d..ff9244d 100644 --- a/src/dotnet/install.sh +++ b/src/dotnet/install.sh @@ -4,47 +4,21 @@ # 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 - +# Docs: https://github.com/devcontainers/features/tree/main/src/dotnet +# Maintainer: The Dev Container spec maintainers DOTNET_VERSION="${VERSION:-"latest"}" -DOTNET_RUNTIME_ONLY="${RUNTIMEONLY:-"false"}" -OVERRIDE_DEFAULT_VERSION="${OVERRIDEDEFAULTVERSION:-"true"}" -INSTALL_USING_APT="${INSTALLUSINGAPT:-"true"}" - -DOTNET_LATEST="7" -DOTNET_LTS="6" - -USERNAME="${USERNAME:-"${_REMOTE_USER:-"automatic"}"}" -UPDATE_RC="${UPDATE_RC:-"true"}" -TARGET_DOTNET_ROOT="${TARGET_DOTNET_ROOT:-"/usr/local/dotnet"}" -ACCESS_GROUP="${ACCESS_GROUP:-"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 jammy" -# 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" -# Ubuntu 22.04 and on do not ship with libssl1.1, which is required for versions of .NET < 6.0 -DOTNET_VERSION_CODENAMES_REQUIRE_OLDER_LIBSSL_1="buster bullseye bionic focal hirsute" - -# Comma-separated list of dotnet versions to be installed -# alongside DOTNET_VERSION, but not set as default. -ADDITIONAL_VERSIONS=${ADDITIONALVERSIONS:-""} - -APT_PREFERENCES_CONTENT="$(cat << 'EOF' -Package: * -Pin: origin "packages.microsoft.com" -Pin-Priority: 1001 -EOF -)" - -APT_PREFERENCES_FILE_ADDED="false" -APT_PREFERENCES_UPDATED="false" +ADDITIONAL_VERSIONS="${ADDITIONALVERSIONS}" +DOTNET_RUNTIME_VERSIONS="${DOTNETRUNTIMEVERSIONS}" +ASPNETCORE_RUNTIME_VERSIONS="${ASPNETCORERUNTIMEVERSIONS}" set -e +# Import trim_whitespace and split_csv +source "scripts/string-helpers.sh" + +# Import install_sdk and install_runtime +source "scripts/dotnet-helpers.sh" + # Clean up rm -rf /var/lib/apt/lists/* @@ -53,72 +27,14 @@ 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 - -# Add dotnet directory to PATH in bashrc/zshrc files if OVERRIDE_DEFAULT_VERSION=true. -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 -} - -apt_get_update() -{ +apt_get_update() { if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then echo "Running apt-get update..." apt-get update -y fi } -# Check if packages are installed and installs them if not. +# Checks if packages are installed and installs them if not check_packages() { if ! dpkg -s "$@" > /dev/null 2>&1; then apt_get_update @@ -126,367 +42,77 @@ check_packages() { 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 - - # Ensure we've exported useful variables - . /etc/os-release - local architecture="$(dpkg --print-architecture)" - - apt-get update -y - 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}")" - 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 \"${partial_package_name}\" (resolved: \"${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 target_dotnet_version="$2" - local use_msft_repo="$3" - - if [ "${use_msft_repo}" = "true" ]; then - # Install dependencies - check_packages apt-transport-https curl ca-certificates gnupg2 dirmngr - # Import key safely and import Microsoft apt repo - 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 - - [ ! -f /etc/apt/preferences ] && APT_PREFERENCES_FILE_ADDED="true" - APT_PREFERENCES_UPDATED="true" - - # See https://github.com/dotnet/core/issues/7699 - echo "${APT_PREFERENCES_CONTENT}" >> /etc/apt/preferences - apt-get update -y - fi - - # .NET 7 is not a LTS version, so handle latest and LTS versions differently - export APT_DOTNET_VERSION="$target_dotnet_version" - if [ "${APT_DOTNET_VERSION}" = "latest" ]; then - APT_DOTNET_VERSION="${DOTNET_LATEST}.0" - elif [ "${APT_DOTNET_VERSION}" = "lts" ]; then - APT_DOTNET_VERSION="${DOTNET_LTS}.0" - fi - - # Sets target_dotnet_version and dotnet_package if matches found. - local base_dotnet_package="dotnet-${sdk_or_runtime}" - export DOTNET_PACKAGE="${base_dotnet_package}" - apt_cache_package_and_version_soft_match APT_DOTNET_VERSION DOTNET_PACKAGE false - if [ "$?" != 0 ] || [ ${DOTNET_PACKAGE} == "${base_dotnet_package}" ]; then - echo "Failed to find requested version." - return 1 - fi - - if type dotnet > /dev/null 2>&1 && [[ "$(dotnet --version)" == *"${APT_DOTNET_VERSION}"* ]] ; then - echo "dotnet version ${APT_DOTNET_VERSION} is already installed" - return 1 - fi - - echo "Installing '${DOTNET_PACKAGE}${APT_DOTNET_VERSION}'..." - apt-get install -yq ${DOTNET_PACKAGE}${APT_DOTNET_VERSION} - if [ "$?" != 0 ]; then - echo "Failed to complete apt install of ${DOTNET_PACKAGE}${TARGET_DOTNET_VERSION}" - return 1 - fi - - # Add symlink for current - CURRENT_DIR="${TARGET_DOTNET_ROOT}/current" - if [[ -d "${CURRENT_DIR}" ]]; then - rm -rf "${CURRENT_DIR}" - fi - mkdir -p "${TARGET_DOTNET_ROOT}" - local dotnet_installed_version="$(dotnet --version)" - # See if its the distro version - if [[ "$(dotnet --info)" == *"Base Path: /usr/lib/dotnet/${DOTNET_SDK_OR_RUNTIME}/${dotnet_installed_version}"* ]]; then - ln -s "/usr/lib/dotnet" "${CURRENT_DIR}" - else - # Location used by MS repo versions - ln -s "/usr/share/dotnet" "${CURRENT_DIR}" - fi -} - -# Find and extract .NET binary download details based on user-requested version -# args: -# sdk_or_runtime $1 -# dotnet_version_to_download $2 -# 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_VERSION="$2" - 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 VERSION to empty string to ensure jq includes all .NET versions in reverse sort below - if [ "${DOTNET_DOWNLOAD_VERSION}" = "latest" ]; then - DOTNET_DOWNLOAD_VERSION="" - fi - - dotnet_patchless_version="$(echo "${DOTNET_DOWNLOAD_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_DOWNLOAD_VERSION}" | grep -o "\." | wc -l)" -lt "2" ]; then - DOTNET_DOWNLOAD_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_DOWNLOAD_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_DOWNLOAD_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_DOWNLOAD_VERSION}" - exit 1 - fi - else - err "Unable to find .NET release details for version ${DOTNET_DOWNLOAD_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" - local version="$2" - - # Check listed package dependencies and install them if they are not already installed. - # NOTE: icu-devtools is a small package with similar dependencies 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 zlib1g - - # Starting with Ubuntu 22.04 (jammy), libssl1.1 does not ship with the OS anymore. - if [[ "${DOTNET_VERSION_CODENAMES_REQUIRE_OLDER_LIBSSL_1}" = *"${VERSION_CODENAME}"* ]]; then - check_packages libssl1.1 - else - check_packages libssl3 - fi - - get_full_version_details "${sdk_or_runtime}" "${version}" - - DOTNET_INSTALL_PATH="${TARGET_DOTNET_ROOT}/${DOTNET_DOWNLOAD_VERSION}" - if [ -d "${DOTNET_INSTALL_PATH}" ]; then - echo "(!) Dotnet version ${DOTNET_DOWNLOAD_VERSION} already exists." - exit 1 - fi - # 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 "${DOTNET_INSTALL_PATH}" - echo "Extract Binary to ${DOTNET_INSTALL_PATH}" - tar -xzf "${TMP_DIR}/${DOTNET_DOWNLOAD_NAME}" -C "${DOTNET_INSTALL_PATH}" --strip-components=1 - - CURRENT_DIR="${TARGET_DOTNET_ROOT}/current" - if [[ ! -d "${CURRENT_DIR}" ]]; then - ln -s "${DOTNET_INSTALL_PATH}" "${CURRENT_DIR}" - fi - - # Give write permissions to the user. - chown -R "${USERNAME}:${USERNAME}" "${CURRENT_DIR}" - chmod g+r+w+s "${CURRENT_DIR}" - chmod -R g+r+w "${CURRENT_DIR}" - - if [[ "${OVERRIDE_DEFAULT_VERSION}" = "true" ]]; then - if [[ $(ls -l ${CURRENT_DIR}) != *"-> ${DOTNET_INSTALL_PATH}"* ]] ; then - rm "${CURRENT_DIR}" - ln -s "${DOTNET_INSTALL_PATH}" "${CURRENT_DIR}" - fi - fi - - updaterc "if [[ \"\${PATH}\" != *\"${CURRENT_DIR}\"* ]]; then export PATH=${CURRENT_DIR}:\${PATH}; fi" -} - -########################### -# Start .NET installation -########################### - -export DEBIAN_FRONTEND=noninteractive - -. /etc/os-release -architecture="$(dpkg --print-architecture)" - -# Dotnet 3.1 and 5 are not supported on Ubuntu 22.04 (jammy)+, -# due to lack of libssl3.0 support. -# See: https://github.com/microsoft/vscode-dev-containers/issues/1458#issuecomment-1135077775 -# NOTE: This will only guard against installation of the dotnet versions we propose via 'features'. -# The user can attempt to install any other version at their own risk. -if [[ "${DOTNET_VERSION}" = "3"* ]] || [[ "${DOTNET_VERSION}" = "5"* ]]; then - if [[ ! "${DOTNET_VERSION_CODENAMES_REQUIRE_OLDER_LIBSSL_1}" = *"${VERSION_CODENAME}"* ]]; then - err "Dotnet ${DOTNET_VERSION} is not supported on Ubuntu ${VERSION_CODENAME} due to a change in the 'libssl' dependency across distributions.\n Please upgrade your version of dotnet, or downgrade your OS version." - exit 1 - fi -fi - -# 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}." +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 -# Install the .NET CLI -echo "(*) Installing .NET CLI..." +# For our own convenience, combine DOTNET_VERSION and ADDITIONAL_VERSIONS into a single 'versions' array +versions=() -CHANGE_OWNERSHIP="false" -if [[ "${DOTNET_ARCHIVE_ARCHITECTURES}" = *"${architecture}"* ]] && [[ "${DOTNET_ARCHIVE_VERSION_CODENAMES}" = *"${VERSION_CODENAME}"* ]] && [[ "${INSTALL_USING_APT}" = "true" ]]; then - echo "Detected ${VERSION_CODENAME} on ${architecture}. Attempting to install dotnet from apt" +# The version can be set to 'none' for runtime-only installations, then the array will just remain empty +# Ensure there are no leading or trailing spaces that can break regex pattern matching +if [ "$DOTNET_VERSION" != "none" ]; then + versions+=("$(trim_whitespace "$DOTNET_VERSION")") + for additional_version in $(split_csv "$ADDITIONAL_VERSIONS"); do + versions+=("$additional_version") + done +fi - install_using_apt "${DOTNET_SDK_OR_RUNTIME}" "${DOTNET_VERSION}" false || install_using_apt "${DOTNET_SDK_OR_RUNTIME}" "${DOTNET_VERSION}" true - if [ "$?" != 0 ]; then - echo "Could not install requested version from apt on current distribution." +dotnetRuntimeVersions=() +for dotnetRuntimeVersion in $(split_csv "$DOTNET_RUNTIME_VERSIONS"); do + dotnetRuntimeVersions+=("$dotnetRuntimeVersion") +done + +aspNetCoreRuntimeVersions=() +for aspNetCoreRuntimeVersion in $(split_csv "$ASPNETCORE_RUNTIME_VERSIONS"); do + aspNetCoreRuntimeVersions+=("$aspNetCoreRuntimeVersion") +done + +# Fail fast in case of bad input to avoid unneccesary work +# v1 of the .NET feature allowed specifying only a major version 'X' like '3' +# v2 removed this ability +# - because install-dotnet.sh does not support it directly +# - because the previous behavior installed an old version like '3.0.103', not the newest version '3.1.426', which was counterintuitive +for version in "${versions[@]}"; do + if [[ "$version" =~ ^[0-9]+$ ]]; then + err "Unsupported .NET SDK version '${version}'. Use 'latest' for the latest version, 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version." exit 1 fi -else - if [[ "${INSTALL_USING_APT}" = "false" ]]; then - echo "Installing dotnet from releases url" - else - echo "Attempting to install dotnet from releases url" +done + +for version in "${dotnetRuntimeVersions[@]}"; do + if [[ "$version" =~ ^[0-9]+$ ]]; then + err "Unsupported .NET Runtime version '${version}'. Use 'latest' for the latest version, 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version." + exit 1 fi - install_using_dotnet_releases_url "${DOTNET_SDK_OR_RUNTIME}" "${DOTNET_VERSION}" - CHANGE_OWNERSHIP="true" -fi +done -# Additional dotnet versions to be installed but not be set as default. -if [ ! -z "${ADDITIONAL_VERSIONS}" ]; then - OLDIFS=$IFS - IFS="," - read -a additional_versions <<< "$ADDITIONAL_VERSIONS" - for version in "${additional_versions[@]}"; do - OVERRIDE_DEFAULT_VERSION="false" - install_using_dotnet_releases_url "${DOTNET_SDK_OR_RUNTIME}" "${version}" - done - IFS=$OLDIFS -fi - -if [ "${CHANGE_OWNERSHIP}" = "true" ]; then - if ! cat /etc/group | grep -e "^dotnet:" > /dev/null 2>&1; then - groupadd -r dotnet +for version in "${aspNetCoreRuntimeVersions[@]}"; do + if [[ "$version" =~ ^[0-9]+$ ]]; then + err "Unsupported ASP.NET Core Runtime version '${version}'. Use 'latest' for the latest version, 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version." + exit 1 fi - usermod -a -G dotnet "${USERNAME}" +done - chown -R "${USERNAME}:dotnet" "${TARGET_DOTNET_ROOT}" - chmod -R g+r+w "${TARGET_DOTNET_ROOT}" - find "${TARGET_DOTNET_ROOT}" -type d -print0 | xargs -n 1 -0 chmod g+s -fi +# Install .NET versions and dependencies +# icu-devtools includes dependencies for .NET +check_packages wget ca-certificates icu-devtools + +for version in "${versions[@]}"; do + install_sdk "$version" +done + +for version in "${dotnetRuntimeVersions[@]}"; do + install_runtime "dotnet" "$version" +done + +for version in "${aspNetCoreRuntimeVersions[@]}"; do + install_runtime "aspnetcore" "$version" +done # Clean up -if [ "${APT_PREFERENCES_UPDATED}" = "true" ]; then - if [ "${APT_PREFERENCES_FILE_ADDED}" = "true" ]; then - rm -f /etc/apt/preferences - else - head -n -3 /etc/apt/preferences > /tmp/preferences-temp && mv /tmp/preferences-temp /etc/apt/preferences - fi -fi - rm -rf /var/lib/apt/lists/* +rm -rf scripts echo "Done!" diff --git a/src/dotnet/scripts/dotnet-helpers.sh b/src/dotnet/scripts/dotnet-helpers.sh new file mode 100644 index 0000000..bda0c9c --- /dev/null +++ b/src/dotnet/scripts/dotnet-helpers.sh @@ -0,0 +1,119 @@ +#!/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/devcontainers/features/tree/main/src/dotnet +# Maintainer: The Dev Container spec maintainers +DOTNET_SCRIPTS=$(dirname "${BASH_SOURCE[0]}") +DOTNET_INSTALL_SCRIPT="$DOTNET_SCRIPTS/vendor/dotnet-install.sh" +DOTNET_INSTALL_DIR='/usr/share/dotnet' + +# Prints the latest dotnet version in the specified channel +# Usage: fetch_latest_version_in_channel [] +# Example: fetch_latest_version_in_channel "LTS" +# Example: fetch_latest_version_in_channel "6.0" "dotnet" +# Example: fetch_latest_version_in_channel "6.0" "aspnetcore" +fetch_latest_version_in_channel() { + local channel="$1" + local runtime="$2" + if [ "$runtime" = "dotnet" ]; then + wget -qO- "https://dotnetcli.azureedge.net/dotnet/Runtime/$channel/latest.version" + elif [ "$runtime" = "aspnetcore" ]; then + wget -qO- "https://dotnetcli.azureedge.net/dotnet/aspnetcore/Runtime/$channel/latest.version" + else + wget -qO- "https://dotnetcli.azureedge.net/dotnet/Sdk/$channel/latest.version" + fi + +} + +# Prints the latest dotnet version +# Usage: fetch_latest_version [] +# Example: fetch_latest_version +# Example: fetch_latest_version "dotnet" +# Example: fetch_latest_version "aspnetcore" +fetch_latest_version() { + local runtime="$1" + local sts_version + local lts_version + sts_version=$(fetch_latest_version_in_channel "STS" "$runtime") + lts_version=$(fetch_latest_version_in_channel "LTS" "$runtime") + if [[ "$sts_version" > "$lts_version" ]]; then + echo "$sts_version" + else + echo "$lts_version" + fi +} + +# Installs a version of the .NET SDK +# Usage: install_sdk +install_sdk() { + local inputVersion="$1" + local version="" + local channel="" + if [[ "$inputVersion" == "latest" ]]; then + # Fetch the latest version manually, because dotnet-install.sh does not support it directly + version=$(fetch_latest_version) + elif [[ "$inputVersion" == "lts" ]]; then + # When user input is 'lts' + # Then version=latest, channel=LTS + version="latest" + channel="LTS" + elif [[ "$inputVersion" =~ ^[0-9]+\.[0-9]+$ ]]; then + # When user input is form 'A.B' like '3.1' + # Then version=latest, channel=3.1 + version="latest" + channel="$inputVersion" + elif [[ "$inputVersion" =~ ^[0-9]+\.[0-9]+\.[0-9]xx$ ]]; then + # When user input is form 'A.B.Cxx' like '6.0.4xx' + # Then version=latest, channel=6.0.4xx + version="latest" + channel="$inputVersion" + else + # Assume version is an exact version string like '6.0.413' or '8.0.100-rc.2.23425.18' + version="$inputVersion" + fi + + # Currently this script does not make it possible to qualify the version, 'GA' is always implied + echo "Executing $DOTNET_INSTALL_SCRIPT --version $version --channel $channel --install-dir $DOTNET_INSTALL_DIR --no-path" + "$DOTNET_INSTALL_SCRIPT" \ + --version "$version" \ + --channel "$channel" \ + --install-dir "$DOTNET_INSTALL_DIR" \ + --no-path +} + +# Installs a version of the .NET Runtime +# Usage: install_runtime +install_runtime() { + local runtime="$1" + local inputVersion="$2" + local version="" + local channel="" + if [[ "$inputVersion" == "latest" ]]; then + # Fetch the latest version manually, because dotnet-install.sh does not support it directly + version=$(fetch_latest_version "$runtime") + elif [[ "$inputVersion" == "lts" ]]; then + # When user input is 'lts' + # Then version=latest, channel=LTS + version="latest" + channel="LTS" + elif [[ "$inputVersion" =~ ^[0-9]+\.[0-9]+$ ]]; then + # When user input is form 'A.B' like '3.1' + # Then version=latest, channel=3.1 + version="latest" + channel="$inputVersion" + else + # Assume version is an exact version string like '6.0.21' or '8.0.0-preview.7.23375.6' + version="$inputVersion" + fi + + echo "Executing $DOTNET_INSTALL_SCRIPT --runtime $runtime --version $version --channel $channel --install-dir $DOTNET_INSTALL_DIR --no-path" + "$DOTNET_INSTALL_SCRIPT" \ + --runtime "$runtime" \ + --version "$version" \ + --channel "$channel" \ + --install-dir "$DOTNET_INSTALL_DIR" \ + --no-path +} diff --git a/src/dotnet/scripts/fetch-latest-dotnet-install.sh b/src/dotnet/scripts/fetch-latest-dotnet-install.sh new file mode 100755 index 0000000..361d539 --- /dev/null +++ b/src/dotnet/scripts/fetch-latest-dotnet-install.sh @@ -0,0 +1,16 @@ +#!/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/devcontainers/features/tree/main/src/dotnet +# Maintainer: The Dev Container spec maintainers +# +# Run this script to replace dotnet-install.sh with the latest and greatest available version +# +DOTNET_SCRIPTS=$(dirname "${BASH_SOURCE[0]}") +DOTNET_INSTALL_SCRIPT="$DOTNET_SCRIPTS/vendor/dotnet-install.sh" + +wget https://dot.net/v1/dotnet-install.sh -O "$DOTNET_INSTALL_SCRIPT" +chmod +x "$DOTNET_INSTALL_SCRIPT" diff --git a/src/dotnet/scripts/string-helpers.sh b/src/dotnet/scripts/string-helpers.sh new file mode 100644 index 0000000..35b4017 --- /dev/null +++ b/src/dotnet/scripts/string-helpers.sh @@ -0,0 +1,42 @@ +#!/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/devcontainers/features/tree/main/src/dotnet +# Maintainer: The Dev Container spec maintainers + +# Removes leading and trailing whitespace from an input string +# Usage: trim_whitespace +trim_whitespace() { + text="$1" + + # Remove leading spaces + while [ "${text:0:1}" == " " ]; do + text="${text:1}" + done + + # Remove trailing spaces + while [ "${text: -1}" == " " ]; do + text="${text:0:-1}" + done + + echo "$text" +} + +# Splits comma-separated values into an array while ignoring empty entries +# Usage: split_csv +split_csv() { + local -a values=() + while IFS="," read -ra entries; do + for entry in "${entries[@]}"; do + entry="$(trim_whitespace "$entry")" + if [ -n "$entry" ]; then + values+=("$entry") + fi + done + done <<< "$1" + + echo "${values[@]}" +} \ No newline at end of file diff --git a/src/dotnet/scripts/vendor/README.md b/src/dotnet/scripts/vendor/README.md new file mode 100644 index 0000000..181b537 --- /dev/null +++ b/src/dotnet/scripts/vendor/README.md @@ -0,0 +1,27 @@ +### **IMPORTANT NOTE** + +Scripts in this directory are sourced externally and not maintained by the Dev Container spec maintainers. Do not make changes directly as they might be overwritten at any moment. + +## dotnet-install.sh + +`dotnet-install.sh` is a copy of . ([Script reference](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script)) + +Quick options reminder for `dotnet-install.sh`: + +- `--version`: `"latest"` (default) or an exact version in the form A.B.C like `"6.0.413"` +- `--channel`: `"LTS"` (default), `"STS"`, a two-part version in the form A.B like `"6.0"` or three-part form A.B.Cxx like `"6.0.1xx"` +- `--quality`: `"daily"`, `"preview"` or `"GA"` +- The channel option is only used when version is 'latest' because an exact version overrides the channel option +- The quality option is only used when channel is 'A.B' or 'A.B.Cxx' because it can't be used with STS or LTS + +Examples + +``` +dotnet-install.sh [--version latest] [--channel LTS] +dotnet-install.sh [--version latest] --channel STS +dotnet-install.sh [--version latest] --channel 6.0 [--quality GA] +dotnet-install.sh [--version latest] --channel 6.0.4xx [--quality GA] +dotnet-install.sh [--version latest] --channel 8.0 --quality preview +dotnet-install.sh [--version latest] --channel 8.0 --quality daily +dotnet-install.sh --version 6.0.413 +``` \ No newline at end of file diff --git a/src/dotnet/scripts/vendor/dotnet-install.sh b/src/dotnet/scripts/vendor/dotnet-install.sh new file mode 100755 index 0000000..a830583 --- /dev/null +++ b/src/dotnet/scripts/vendor/dotnet-install.sh @@ -0,0 +1,1746 @@ +#!/usr/bin/env bash +# Copyright (c) .NET Foundation and contributors. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# + +# Stop script on NZEC +set -e +# Stop script if unbound variable found (use ${var:-} if intentional) +set -u +# By default cmd1 | cmd2 returns exit code of cmd2 regardless of cmd1 success +# This is causing it to fail +set -o pipefail + +# Use in the the functions: eval $invocation +invocation='say_verbose "Calling: ${yellow:-}${FUNCNAME[0]} ${green:-}$*${normal:-}"' + +# standard output may be used as a return value in the functions +# we need a way to write text on the screen in the functions so that +# it won't interfere with the return value. +# Exposing stream 3 as a pipe to standard output of the script itself +exec 3>&1 + +# Setup some colors to use. These need to work in fairly limited shells, like the Ubuntu Docker container where there are only 8 colors. +# See if stdout is a terminal +if [ -t 1 ] && command -v tput > /dev/null; then + # see if it supports colors + ncolors=$(tput colors || echo 0) + if [ -n "$ncolors" ] && [ $ncolors -ge 8 ]; then + bold="$(tput bold || echo)" + normal="$(tput sgr0 || echo)" + black="$(tput setaf 0 || echo)" + red="$(tput setaf 1 || echo)" + green="$(tput setaf 2 || echo)" + yellow="$(tput setaf 3 || echo)" + blue="$(tput setaf 4 || echo)" + magenta="$(tput setaf 5 || echo)" + cyan="$(tput setaf 6 || echo)" + white="$(tput setaf 7 || echo)" + fi +fi + +say_warning() { + printf "%b\n" "${yellow:-}dotnet_install: Warning: $1${normal:-}" >&3 +} + +say_err() { + printf "%b\n" "${red:-}dotnet_install: Error: $1${normal:-}" >&2 +} + +say() { + # using stream 3 (defined in the beginning) to not interfere with stdout of functions + # which may be used as return value + printf "%b\n" "${cyan:-}dotnet-install:${normal:-} $1" >&3 +} + +say_verbose() { + if [ "$verbose" = true ]; then + say "$1" + fi +} + +# This platform list is finite - if the SDK/Runtime has supported Linux distribution-specific assets, +# then and only then should the Linux distribution appear in this list. +# Adding a Linux distribution to this list does not imply distribution-specific support. +get_legacy_os_name_from_platform() { + eval $invocation + + platform="$1" + case "$platform" in + "centos.7") + echo "centos" + return 0 + ;; + "debian.8") + echo "debian" + return 0 + ;; + "debian.9") + echo "debian.9" + return 0 + ;; + "fedora.23") + echo "fedora.23" + return 0 + ;; + "fedora.24") + echo "fedora.24" + return 0 + ;; + "fedora.27") + echo "fedora.27" + return 0 + ;; + "fedora.28") + echo "fedora.28" + return 0 + ;; + "opensuse.13.2") + echo "opensuse.13.2" + return 0 + ;; + "opensuse.42.1") + echo "opensuse.42.1" + return 0 + ;; + "opensuse.42.3") + echo "opensuse.42.3" + return 0 + ;; + "rhel.7"*) + echo "rhel" + return 0 + ;; + "ubuntu.14.04") + echo "ubuntu" + return 0 + ;; + "ubuntu.16.04") + echo "ubuntu.16.04" + return 0 + ;; + "ubuntu.16.10") + echo "ubuntu.16.10" + return 0 + ;; + "ubuntu.18.04") + echo "ubuntu.18.04" + return 0 + ;; + "alpine.3.4.3") + echo "alpine" + return 0 + ;; + esac + return 1 +} + +get_legacy_os_name() { + eval $invocation + + local uname=$(uname) + if [ "$uname" = "Darwin" ]; then + echo "osx" + return 0 + elif [ -n "$runtime_id" ]; then + echo $(get_legacy_os_name_from_platform "${runtime_id%-*}" || echo "${runtime_id%-*}") + return 0 + else + if [ -e /etc/os-release ]; then + . /etc/os-release + os=$(get_legacy_os_name_from_platform "$ID${VERSION_ID:+.${VERSION_ID}}" || echo "") + if [ -n "$os" ]; then + echo "$os" + return 0 + fi + fi + fi + + say_verbose "Distribution specific OS name and version could not be detected: UName = $uname" + return 1 +} + +get_linux_platform_name() { + eval $invocation + + if [ -n "$runtime_id" ]; then + echo "${runtime_id%-*}" + return 0 + else + if [ -e /etc/os-release ]; then + . /etc/os-release + echo "$ID${VERSION_ID:+.${VERSION_ID}}" + return 0 + elif [ -e /etc/redhat-release ]; then + local redhatRelease=$(&1 || true) | grep -q musl +} + +get_current_os_name() { + eval $invocation + + local uname=$(uname) + if [ "$uname" = "Darwin" ]; then + echo "osx" + return 0 + elif [ "$uname" = "FreeBSD" ]; then + echo "freebsd" + return 0 + elif [ "$uname" = "Linux" ]; then + local linux_platform_name="" + linux_platform_name="$(get_linux_platform_name)" || true + + if [ "$linux_platform_name" = "rhel.6" ]; then + echo $linux_platform_name + return 0 + elif is_musl_based_distro; then + echo "linux-musl" + return 0 + elif [ "$linux_platform_name" = "linux-musl" ]; then + echo "linux-musl" + return 0 + else + echo "linux" + return 0 + fi + fi + + say_err "OS name could not be detected: UName = $uname" + return 1 +} + +machine_has() { + eval $invocation + + command -v "$1" > /dev/null 2>&1 + return $? +} + +check_min_reqs() { + local hasMinimum=false + if machine_has "curl"; then + hasMinimum=true + elif machine_has "wget"; then + hasMinimum=true + fi + + if [ "$hasMinimum" = "false" ]; then + say_err "curl (recommended) or wget are required to download dotnet. Install missing prerequisite to proceed." + return 1 + fi + return 0 +} + +# args: +# input - $1 +to_lowercase() { + #eval $invocation + + echo "$1" | tr '[:upper:]' '[:lower:]' + return 0 +} + +# args: +# input - $1 +remove_trailing_slash() { + #eval $invocation + + local input="${1:-}" + echo "${input%/}" + return 0 +} + +# args: +# input - $1 +remove_beginning_slash() { + #eval $invocation + + local input="${1:-}" + echo "${input#/}" + return 0 +} + +# args: +# root_path - $1 +# child_path - $2 - this parameter can be empty +combine_paths() { + eval $invocation + + # TODO: Consider making it work with any number of paths. For now: + if [ ! -z "${3:-}" ]; then + say_err "combine_paths: Function takes two parameters." + return 1 + fi + + local root_path="$(remove_trailing_slash "$1")" + local child_path="$(remove_beginning_slash "${2:-}")" + say_verbose "combine_paths: root_path=$root_path" + say_verbose "combine_paths: child_path=$child_path" + echo "$root_path/$child_path" + return 0 +} + +get_machine_architecture() { + eval $invocation + + if command -v uname > /dev/null; then + CPUName=$(uname -m) + case $CPUName in + armv*l) + echo "arm" + return 0 + ;; + aarch64|arm64) + echo "arm64" + return 0 + ;; + s390x) + echo "s390x" + return 0 + ;; + ppc64le) + echo "ppc64le" + return 0 + ;; + esac + fi + + # Always default to 'x64' + echo "x64" + return 0 +} + +# args: +# architecture - $1 +get_normalized_architecture_from_architecture() { + eval $invocation + + local architecture="$(to_lowercase "$1")" + + if [[ $architecture == \ ]]; then + echo "$(get_machine_architecture)" + return 0 + fi + + case "$architecture" in + amd64|x64) + echo "x64" + return 0 + ;; + arm) + echo "arm" + return 0 + ;; + arm64) + echo "arm64" + return 0 + ;; + s390x) + echo "s390x" + return 0 + ;; + ppc64le) + echo "ppc64le" + return 0 + ;; + esac + + say_err "Architecture \`$architecture\` not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues" + return 1 +} + +# args: +# version - $1 +# channel - $2 +# architecture - $3 +get_normalized_architecture_for_specific_sdk_version() { + eval $invocation + + local is_version_support_arm64="$(is_arm64_supported "$1")" + local is_channel_support_arm64="$(is_arm64_supported "$2")" + local architecture="$3"; + local osname="$(get_current_os_name)" + + if [ "$osname" == "osx" ] && [ "$architecture" == "arm64" ] && { [ "$is_version_support_arm64" = false ] || [ "$is_channel_support_arm64" = false ]; }; then + #check if rosetta is installed + if [ "$(/usr/bin/pgrep oahd >/dev/null 2>&1;echo $?)" -eq 0 ]; then + say_verbose "Changing user architecture from '$architecture' to 'x64' because .NET SDKs prior to version 6.0 do not support arm64." + echo "x64" + return 0; + else + say_err "Architecture \`$architecture\` is not supported for .NET SDK version \`$version\`. Please install Rosetta to allow emulation of the \`$architecture\` .NET SDK on this platform" + return 1 + fi + fi + + echo "$architecture" + return 0 +} + +# args: +# version or channel - $1 +is_arm64_supported() { + #any channel or version that starts with the specified versions + case "$1" in + ( "1"* | "2"* | "3"* | "4"* | "5"*) + echo false + return 0 + esac + + echo true + return 0 +} + +# args: +# user_defined_os - $1 +get_normalized_os() { + eval $invocation + + local osname="$(to_lowercase "$1")" + if [ ! -z "$osname" ]; then + case "$osname" in + osx | freebsd | rhel.6 | linux-musl | linux) + echo "$osname" + return 0 + ;; + *) + say_err "'$user_defined_os' is not a supported value for --os option, supported values are: osx, linux, linux-musl, freebsd, rhel.6. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues." + return 1 + ;; + esac + else + osname="$(get_current_os_name)" || return 1 + fi + echo "$osname" + return 0 +} + +# args: +# quality - $1 +get_normalized_quality() { + eval $invocation + + local quality="$(to_lowercase "$1")" + if [ ! -z "$quality" ]; then + case "$quality" in + daily | signed | validated | preview) + echo "$quality" + return 0 + ;; + ga) + #ga quality is available without specifying quality, so normalizing it to empty + return 0 + ;; + *) + say_err "'$quality' is not a supported value for --quality option. Supported values are: daily, signed, validated, preview, ga. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues." + return 1 + ;; + esac + fi + return 0 +} + +# args: +# channel - $1 +get_normalized_channel() { + eval $invocation + + local channel="$(to_lowercase "$1")" + + if [[ $channel == current ]]; then + say_warning 'Value "Current" is deprecated for -Channel option. Use "STS" instead.' + fi + + if [[ $channel == release/* ]]; then + say_warning 'Using branch name with -Channel option is no longer supported with newer releases. Use -Quality option with a channel in X.Y format instead.'; + fi + + if [ ! -z "$channel" ]; then + case "$channel" in + lts) + echo "LTS" + return 0 + ;; + sts) + echo "STS" + return 0 + ;; + current) + echo "STS" + return 0 + ;; + *) + echo "$channel" + return 0 + ;; + esac + fi + + return 0 +} + +# args: +# runtime - $1 +get_normalized_product() { + eval $invocation + + local product="" + local runtime="$(to_lowercase "$1")" + if [[ "$runtime" == "dotnet" ]]; then + product="dotnet-runtime" + elif [[ "$runtime" == "aspnetcore" ]]; then + product="aspnetcore-runtime" + elif [ -z "$runtime" ]; then + product="dotnet-sdk" + fi + echo "$product" + return 0 +} + +# The version text returned from the feeds is a 1-line or 2-line string: +# For the SDK and the dotnet runtime (2 lines): +# Line 1: # commit_hash +# Line 2: # 4-part version +# For the aspnetcore runtime (1 line): +# Line 1: # 4-part version + +# args: +# version_text - stdin +get_version_from_latestversion_file_content() { + eval $invocation + + cat | tail -n 1 | sed 's/\r$//' + return 0 +} + +# args: +# install_root - $1 +# relative_path_to_package - $2 +# specific_version - $3 +is_dotnet_package_installed() { + eval $invocation + + local install_root="$1" + local relative_path_to_package="$2" + local specific_version="${3//[$'\t\r\n']}" + + local dotnet_package_path="$(combine_paths "$(combine_paths "$install_root" "$relative_path_to_package")" "$specific_version")" + say_verbose "is_dotnet_package_installed: dotnet_package_path=$dotnet_package_path" + + if [ -d "$dotnet_package_path" ]; then + return 0 + else + return 1 + fi +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +get_version_from_latestversion_file() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + + local version_file_url=null + if [[ "$runtime" == "dotnet" ]]; then + version_file_url="$azure_feed/Runtime/$channel/latest.version" + elif [[ "$runtime" == "aspnetcore" ]]; then + version_file_url="$azure_feed/aspnetcore/Runtime/$channel/latest.version" + elif [ -z "$runtime" ]; then + version_file_url="$azure_feed/Sdk/$channel/latest.version" + else + say_err "Invalid value for \$runtime" + return 1 + fi + say_verbose "get_version_from_latestversion_file: latest url: $version_file_url" + + download "$version_file_url" || return $? + return 0 +} + +# args: +# json_file - $1 +parse_globaljson_file_for_version() { + eval $invocation + + local json_file="$1" + if [ ! -f "$json_file" ]; then + say_err "Unable to find \`$json_file\`" + return 1 + fi + + sdk_section=$(cat $json_file | tr -d "\r" | awk '/"sdk"/,/}/') + if [ -z "$sdk_section" ]; then + say_err "Unable to parse the SDK node in \`$json_file\`" + return 1 + fi + + sdk_list=$(echo $sdk_section | awk -F"[{}]" '{print $2}') + sdk_list=${sdk_list//[\" ]/} + sdk_list=${sdk_list//,/$'\n'} + + local version_info="" + while read -r line; do + IFS=: + while read -r key value; do + if [[ "$key" == "version" ]]; then + version_info=$value + fi + done <<< "$line" + done <<< "$sdk_list" + if [ -z "$version_info" ]; then + say_err "Unable to find the SDK:version node in \`$json_file\`" + return 1 + fi + + unset IFS; + echo "$version_info" + return 0 +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +# version - $4 +# json_file - $5 +get_specific_version_from_version() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + local version="$(to_lowercase "$4")" + local json_file="$5" + + if [ -z "$json_file" ]; then + if [[ "$version" == "latest" ]]; then + local version_info + version_info="$(get_version_from_latestversion_file "$azure_feed" "$channel" "$normalized_architecture" false)" || return 1 + say_verbose "get_specific_version_from_version: version_info=$version_info" + echo "$version_info" | get_version_from_latestversion_file_content + return 0 + else + echo "$version" + return 0 + fi + else + local version_info + version_info="$(parse_globaljson_file_for_version "$json_file")" || return 1 + echo "$version_info" + return 0 + fi +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +# specific_version - $4 +# normalized_os - $5 +construct_download_link() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + local specific_version="${4//[$'\t\r\n']}" + local specific_product_version="$(get_specific_product_version "$1" "$4")" + local osname="$5" + + local download_link=null + if [[ "$runtime" == "dotnet" ]]; then + download_link="$azure_feed/Runtime/$specific_version/dotnet-runtime-$specific_product_version-$osname-$normalized_architecture.tar.gz" + elif [[ "$runtime" == "aspnetcore" ]]; then + download_link="$azure_feed/aspnetcore/Runtime/$specific_version/aspnetcore-runtime-$specific_product_version-$osname-$normalized_architecture.tar.gz" + elif [ -z "$runtime" ]; then + download_link="$azure_feed/Sdk/$specific_version/dotnet-sdk-$specific_product_version-$osname-$normalized_architecture.tar.gz" + else + return 1 + fi + + echo "$download_link" + return 0 +} + +# args: +# azure_feed - $1 +# specific_version - $2 +# download link - $3 (optional) +get_specific_product_version() { + # If we find a 'productVersion.txt' at the root of any folder, we'll use its contents + # to resolve the version of what's in the folder, superseding the specified version. + # if 'productVersion.txt' is missing but download link is already available, product version will be taken from download link + eval $invocation + + local azure_feed="$1" + local specific_version="${2//[$'\t\r\n']}" + local package_download_link="" + if [ $# -gt 2 ]; then + local package_download_link="$3" + fi + local specific_product_version=null + + # Try to get the version number, using the productVersion.txt file located next to the installer file. + local download_links=($(get_specific_product_version_url "$azure_feed" "$specific_version" true "$package_download_link") + $(get_specific_product_version_url "$azure_feed" "$specific_version" false "$package_download_link")) + + for download_link in "${download_links[@]}" + do + say_verbose "Checking for the existence of $download_link" + + if machine_has "curl" + then + if ! specific_product_version=$(curl -s --fail "${download_link}${feed_credential}" 2>&1); then + continue + else + echo "${specific_product_version//[$'\t\r\n']}" + return 0 + fi + + elif machine_has "wget" + then + specific_product_version=$(wget -qO- "${download_link}${feed_credential}" 2>&1) + if [ $? = 0 ]; then + echo "${specific_product_version//[$'\t\r\n']}" + return 0 + fi + fi + done + + # Getting the version number with productVersion.txt has failed. Try parsing the download link for a version number. + say_verbose "Failed to get the version using productVersion.txt file. Download link will be parsed instead." + specific_product_version="$(get_product_specific_version_from_download_link "$package_download_link" "$specific_version")" + echo "${specific_product_version//[$'\t\r\n']}" + return 0 +} + +# args: +# azure_feed - $1 +# specific_version - $2 +# is_flattened - $3 +# download link - $4 (optional) +get_specific_product_version_url() { + eval $invocation + + local azure_feed="$1" + local specific_version="$2" + local is_flattened="$3" + local package_download_link="" + if [ $# -gt 3 ]; then + local package_download_link="$4" + fi + + local pvFileName="productVersion.txt" + if [ "$is_flattened" = true ]; then + if [ -z "$runtime" ]; then + pvFileName="sdk-productVersion.txt" + elif [[ "$runtime" == "dotnet" ]]; then + pvFileName="runtime-productVersion.txt" + else + pvFileName="$runtime-productVersion.txt" + fi + fi + + local download_link=null + + if [ -z "$package_download_link" ]; then + if [[ "$runtime" == "dotnet" ]]; then + download_link="$azure_feed/Runtime/$specific_version/${pvFileName}" + elif [[ "$runtime" == "aspnetcore" ]]; then + download_link="$azure_feed/aspnetcore/Runtime/$specific_version/${pvFileName}" + elif [ -z "$runtime" ]; then + download_link="$azure_feed/Sdk/$specific_version/${pvFileName}" + else + return 1 + fi + else + download_link="${package_download_link%/*}/${pvFileName}" + fi + + say_verbose "Constructed productVersion link: $download_link" + echo "$download_link" + return 0 +} + +# args: +# download link - $1 +# specific version - $2 +get_product_specific_version_from_download_link() +{ + eval $invocation + + local download_link="$1" + local specific_version="$2" + local specific_product_version="" + + if [ -z "$download_link" ]; then + echo "$specific_version" + return 0 + fi + + #get filename + filename="${download_link##*/}" + + #product specific version follows the product name + #for filename 'dotnet-sdk-3.1.404-linux-x64.tar.gz': the product version is 3.1.404 + IFS='-' + read -ra filename_elems <<< "$filename" + count=${#filename_elems[@]} + if [[ "$count" -gt 2 ]]; then + specific_product_version="${filename_elems[2]}" + else + specific_product_version=$specific_version + fi + unset IFS; + echo "$specific_product_version" + return 0 +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +# specific_version - $4 +construct_legacy_download_link() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + local specific_version="${4//[$'\t\r\n']}" + + local distro_specific_osname + distro_specific_osname="$(get_legacy_os_name)" || return 1 + + local legacy_download_link=null + if [[ "$runtime" == "dotnet" ]]; then + legacy_download_link="$azure_feed/Runtime/$specific_version/dotnet-$distro_specific_osname-$normalized_architecture.$specific_version.tar.gz" + elif [ -z "$runtime" ]; then + legacy_download_link="$azure_feed/Sdk/$specific_version/dotnet-dev-$distro_specific_osname-$normalized_architecture.$specific_version.tar.gz" + else + return 1 + fi + + echo "$legacy_download_link" + return 0 +} + +get_user_install_path() { + eval $invocation + + if [ ! -z "${DOTNET_INSTALL_DIR:-}" ]; then + echo "$DOTNET_INSTALL_DIR" + else + echo "$HOME/.dotnet" + fi + return 0 +} + +# args: +# install_dir - $1 +resolve_installation_path() { + eval $invocation + + local install_dir=$1 + if [ "$install_dir" = "" ]; then + local user_install_path="$(get_user_install_path)" + say_verbose "resolve_installation_path: user_install_path=$user_install_path" + echo "$user_install_path" + return 0 + fi + + echo "$install_dir" + return 0 +} + +# args: +# relative_or_absolute_path - $1 +get_absolute_path() { + eval $invocation + + local relative_or_absolute_path=$1 + echo "$(cd "$(dirname "$1")" && pwd -P)/$(basename "$1")" + return 0 +} + +# args: +# input_files - stdin +# root_path - $1 +# out_path - $2 +# override - $3 +copy_files_or_dirs_from_list() { + eval $invocation + + local root_path="$(remove_trailing_slash "$1")" + local out_path="$(remove_trailing_slash "$2")" + local override="$3" + local osname="$(get_current_os_name)" + local override_switch=$( + if [ "$override" = false ]; then + if [ "$osname" = "linux-musl" ]; then + printf -- "-u"; + else + printf -- "-n"; + fi + fi) + + cat | uniq | while read -r file_path; do + local path="$(remove_beginning_slash "${file_path#$root_path}")" + local target="$out_path/$path" + if [ "$override" = true ] || (! ([ -d "$target" ] || [ -e "$target" ])); then + mkdir -p "$out_path/$(dirname "$path")" + if [ -d "$target" ]; then + rm -rf "$target" + fi + cp -R $override_switch "$root_path/$path" "$target" + fi + done +} + +# args: +# zip_path - $1 +# out_path - $2 +extract_dotnet_package() { + eval $invocation + + local zip_path="$1" + local out_path="$2" + + local temp_out_path="$(mktemp -d "$temporary_file_template")" + + local failed=false + tar -xzf "$zip_path" -C "$temp_out_path" > /dev/null || failed=true + + local folders_with_version_regex='^.*/[0-9]+\.[0-9]+[^/]+/' + find "$temp_out_path" -type f | grep -Eo "$folders_with_version_regex" | sort | copy_files_or_dirs_from_list "$temp_out_path" "$out_path" false + find "$temp_out_path" -type f | grep -Ev "$folders_with_version_regex" | copy_files_or_dirs_from_list "$temp_out_path" "$out_path" "$override_non_versioned_files" + + rm -rf "$temp_out_path" + rm -f "$zip_path" && say_verbose "Temporary zip file $zip_path was removed" + + if [ "$failed" = true ]; then + say_err "Extraction failed" + return 1 + fi + return 0 +} + +# args: +# remote_path - $1 +# disable_feed_credential - $2 +get_http_header() +{ + eval $invocation + local remote_path="$1" + local disable_feed_credential="$2" + + local failed=false + local response + if machine_has "curl"; then + get_http_header_curl $remote_path $disable_feed_credential || failed=true + elif machine_has "wget"; then + get_http_header_wget $remote_path $disable_feed_credential || failed=true + else + failed=true + fi + if [ "$failed" = true ]; then + say_verbose "Failed to get HTTP header: '$remote_path'." + return 1 + fi + return 0 +} + +# args: +# remote_path - $1 +# disable_feed_credential - $2 +get_http_header_curl() { + eval $invocation + local remote_path="$1" + local disable_feed_credential="$2" + + remote_path_with_credential="$remote_path" + if [ "$disable_feed_credential" = false ]; then + remote_path_with_credential+="$feed_credential" + fi + + curl_options="-I -sSL --retry 5 --retry-delay 2 --connect-timeout 15 " + curl $curl_options "$remote_path_with_credential" 2>&1 || return 1 + return 0 +} + +# args: +# remote_path - $1 +# disable_feed_credential - $2 +get_http_header_wget() { + eval $invocation + local remote_path="$1" + local disable_feed_credential="$2" + local wget_options="-q -S --spider --tries 5 " + + local wget_options_extra='' + + # Test for options that aren't supported on all wget implementations. + if [[ $(wget -h 2>&1 | grep -E 'waitretry|connect-timeout') ]]; then + wget_options_extra="--waitretry 2 --connect-timeout 15 " + else + say "wget extra options are unavailable for this environment" + fi + + remote_path_with_credential="$remote_path" + if [ "$disable_feed_credential" = false ]; then + remote_path_with_credential+="$feed_credential" + fi + + wget $wget_options $wget_options_extra "$remote_path_with_credential" 2>&1 + + return $? +} + +# args: +# remote_path - $1 +# [out_path] - $2 - stdout if not provided +download() { + eval $invocation + + local remote_path="$1" + local out_path="${2:-}" + + if [[ "$remote_path" != "http"* ]]; then + cp "$remote_path" "$out_path" + return $? + fi + + local failed=false + local attempts=0 + while [ $attempts -lt 3 ]; do + attempts=$((attempts+1)) + failed=false + if machine_has "curl"; then + downloadcurl "$remote_path" "$out_path" || failed=true + elif machine_has "wget"; then + downloadwget "$remote_path" "$out_path" || failed=true + else + say_err "Missing dependency: neither curl nor wget was found." + exit 1 + fi + + if [ "$failed" = false ] || [ $attempts -ge 3 ] || { [ ! -z $http_code ] && [ $http_code = "404" ]; }; then + break + fi + + say "Download attempt #$attempts has failed: $http_code $download_error_msg" + say "Attempt #$((attempts+1)) will start in $((attempts*10)) seconds." + sleep $((attempts*10)) + done + + if [ "$failed" = true ]; then + say_verbose "Download failed: $remote_path" + return 1 + fi + return 0 +} + +# Updates global variables $http_code and $download_error_msg +downloadcurl() { + eval $invocation + unset http_code + unset download_error_msg + local remote_path="$1" + local out_path="${2:-}" + # Append feed_credential as late as possible before calling curl to avoid logging feed_credential + # Avoid passing URI with credentials to functions: note, most of them echoing parameters of invocation in verbose output. + local remote_path_with_credential="${remote_path}${feed_credential}" + local curl_options="--retry 20 --retry-delay 2 --connect-timeout 15 -sSL -f --create-dirs " + local curl_exit_code=0; + if [ -z "$out_path" ]; then + curl $curl_options "$remote_path_with_credential" 2>&1 + curl_exit_code=$? + else + curl $curl_options -o "$out_path" "$remote_path_with_credential" 2>&1 + curl_exit_code=$? + fi + + if [ $curl_exit_code -gt 0 ]; then + download_error_msg="Unable to download $remote_path." + # Check for curl timeout codes + if [[ $curl_exit_code == 7 || $curl_exit_code == 28 ]]; then + download_error_msg+=" Failed to reach the server: connection timeout." + else + local disable_feed_credential=false + local response=$(get_http_header_curl $remote_path $disable_feed_credential) + http_code=$( echo "$response" | awk '/^HTTP/{print $2}' | tail -1 ) + if [[ ! -z $http_code && $http_code != 2* ]]; then + download_error_msg+=" Returned HTTP status code: $http_code." + fi + fi + say_verbose "$download_error_msg" + return 1 + fi + return 0 +} + + +# Updates global variables $http_code and $download_error_msg +downloadwget() { + eval $invocation + unset http_code + unset download_error_msg + local remote_path="$1" + local out_path="${2:-}" + # Append feed_credential as late as possible before calling wget to avoid logging feed_credential + local remote_path_with_credential="${remote_path}${feed_credential}" + local wget_options="--tries 20 " + + local wget_options_extra='' + local wget_result='' + + # Test for options that aren't supported on all wget implementations. + if [[ $(wget -h 2>&1 | grep -E 'waitretry|connect-timeout') ]]; then + wget_options_extra="--waitretry 2 --connect-timeout 15 " + else + say "wget extra options are unavailable for this environment" + fi + + if [ -z "$out_path" ]; then + wget -q $wget_options $wget_options_extra -O - "$remote_path_with_credential" 2>&1 + wget_result=$? + else + wget $wget_options $wget_options_extra -O "$out_path" "$remote_path_with_credential" 2>&1 + wget_result=$? + fi + + if [[ $wget_result != 0 ]]; then + local disable_feed_credential=false + local response=$(get_http_header_wget $remote_path $disable_feed_credential) + http_code=$( echo "$response" | awk '/^ HTTP/{print $2}' | tail -1 ) + download_error_msg="Unable to download $remote_path." + if [[ ! -z $http_code && $http_code != 2* ]]; then + download_error_msg+=" Returned HTTP status code: $http_code." + # wget exit code 4 stands for network-issue + elif [[ $wget_result == 4 ]]; then + download_error_msg+=" Failed to reach the server: connection timeout." + fi + say_verbose "$download_error_msg" + return 1 + fi + + return 0 +} + +get_download_link_from_aka_ms() { + eval $invocation + + #quality is not supported for LTS or STS channel + #STS maps to current + if [[ ! -z "$normalized_quality" && ("$normalized_channel" == "LTS" || "$normalized_channel" == "STS") ]]; then + normalized_quality="" + say_warning "Specifying quality for STS or LTS channel is not supported, the quality will be ignored." + fi + + say_verbose "Retrieving primary payload URL from aka.ms for channel: '$normalized_channel', quality: '$normalized_quality', product: '$normalized_product', os: '$normalized_os', architecture: '$normalized_architecture'." + + #construct aka.ms link + aka_ms_link="https://aka.ms/dotnet" + if [ "$internal" = true ]; then + aka_ms_link="$aka_ms_link/internal" + fi + aka_ms_link="$aka_ms_link/$normalized_channel" + if [[ ! -z "$normalized_quality" ]]; then + aka_ms_link="$aka_ms_link/$normalized_quality" + fi + aka_ms_link="$aka_ms_link/$normalized_product-$normalized_os-$normalized_architecture.tar.gz" + say_verbose "Constructed aka.ms link: '$aka_ms_link'." + + #get HTTP response + #do not pass credentials as a part of the $aka_ms_link and do not apply credentials in the get_http_header function + #otherwise the redirect link would have credentials as well + #it would result in applying credentials twice to the resulting link and thus breaking it, and in echoing credentials to the output as a part of redirect link + disable_feed_credential=true + response="$(get_http_header $aka_ms_link $disable_feed_credential)" + + say_verbose "Received response: $response" + # Get results of all the redirects. + http_codes=$( echo "$response" | awk '$1 ~ /^HTTP/ {print $2}' ) + # They all need to be 301, otherwise some links are broken (except for the last, which is not a redirect but 200 or 404). + broken_redirects=$( echo "$http_codes" | sed '$d' | grep -v '301' ) + + # All HTTP codes are 301 (Moved Permanently), the redirect link exists. + if [[ -z "$broken_redirects" ]]; then + aka_ms_download_link=$( echo "$response" | awk '$1 ~ /^Location/{print $2}' | tail -1 | tr -d '\r') + + if [[ -z "$aka_ms_download_link" ]]; then + say_verbose "The aka.ms link '$aka_ms_link' is not valid: failed to get redirect location." + return 1 + fi + + say_verbose "The redirect location retrieved: '$aka_ms_download_link'." + return 0 + else + say_verbose "The aka.ms link '$aka_ms_link' is not valid: received HTTP code: $(echo "$broken_redirects" | paste -sd "," -)." + return 1 + fi +} + +get_feeds_to_use() +{ + feeds=( + "https://dotnetcli.azureedge.net/dotnet" + "https://dotnetbuilds.azureedge.net/public" + ) + + if [[ -n "$azure_feed" ]]; then + feeds=("$azure_feed") + fi + + if [[ "$no_cdn" == "true" ]]; then + feeds=( + "https://dotnetcli.blob.core.windows.net/dotnet" + "https://dotnetbuilds.blob.core.windows.net/public" + ) + + if [[ -n "$uncached_feed" ]]; then + feeds=("$uncached_feed") + fi + fi +} + +# THIS FUNCTION MAY EXIT (if the determined version is already installed). +generate_download_links() { + + download_links=() + specific_versions=() + effective_versions=() + link_types=() + + # If generate_akams_links returns false, no fallback to old links. Just terminate. + # This function may also 'exit' (if the determined version is already installed). + generate_akams_links || return + + # Check other feeds only if we haven't been able to find an aka.ms link. + if [[ "${#download_links[@]}" -lt 1 ]]; then + for feed in ${feeds[@]} + do + # generate_regular_links may also 'exit' (if the determined version is already installed). + generate_regular_links $feed || return + done + fi + + if [[ "${#download_links[@]}" -eq 0 ]]; then + say_err "Failed to resolve the exact version number." + return 1 + fi + + say_verbose "Generated ${#download_links[@]} links." + for link_index in ${!download_links[@]} + do + say_verbose "Link $link_index: ${link_types[$link_index]}, ${effective_versions[$link_index]}, ${download_links[$link_index]}" + done +} + +# THIS FUNCTION MAY EXIT (if the determined version is already installed). +generate_akams_links() { + local valid_aka_ms_link=true; + + normalized_version="$(to_lowercase "$version")" + if [[ "$normalized_version" != "latest" ]] && [ -n "$normalized_quality" ]; then + say_err "Quality and Version options are not allowed to be specified simultaneously. See https://learn.microsoft.com/dotnet/core/tools/dotnet-install-script#options for details." + return 1 + fi + + if [[ -n "$json_file" || "$normalized_version" != "latest" ]]; then + # aka.ms links are not needed when exact version is specified via command or json file + return + fi + + get_download_link_from_aka_ms || valid_aka_ms_link=false + + if [[ "$valid_aka_ms_link" == true ]]; then + say_verbose "Retrieved primary payload URL from aka.ms link: '$aka_ms_download_link'." + say_verbose "Downloading using legacy url will not be attempted." + + download_link=$aka_ms_download_link + + #get version from the path + IFS='/' + read -ra pathElems <<< "$download_link" + count=${#pathElems[@]} + specific_version="${pathElems[count-2]}" + unset IFS; + say_verbose "Version: '$specific_version'." + + #Retrieve effective version + effective_version="$(get_specific_product_version "$azure_feed" "$specific_version" "$download_link")" + + # Add link info to arrays + download_links+=($download_link) + specific_versions+=($specific_version) + effective_versions+=($effective_version) + link_types+=("aka.ms") + + # Check if the SDK version is already installed. + if [[ "$dry_run" != true ]] && is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then + say "$asset_name with version '$effective_version' is already installed." + exit 0 + fi + + return 0 + fi + + # if quality is specified - exit with error - there is no fallback approach + if [ ! -z "$normalized_quality" ]; then + say_err "Failed to locate the latest version in the channel '$normalized_channel' with '$normalized_quality' quality for '$normalized_product', os: '$normalized_os', architecture: '$normalized_architecture'." + say_err "Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support." + return 1 + fi + say_verbose "Falling back to latest.version file approach." +} + +# THIS FUNCTION MAY EXIT (if the determined version is already installed) +# args: +# feed - $1 +generate_regular_links() { + local feed="$1" + local valid_legacy_download_link=true + + specific_version=$(get_specific_version_from_version "$feed" "$channel" "$normalized_architecture" "$version" "$json_file") || specific_version='0' + + if [[ "$specific_version" == '0' ]]; then + say_verbose "Failed to resolve the specific version number using feed '$feed'" + return + fi + + effective_version="$(get_specific_product_version "$feed" "$specific_version")" + say_verbose "specific_version=$specific_version" + + download_link="$(construct_download_link "$feed" "$channel" "$normalized_architecture" "$specific_version" "$normalized_os")" + say_verbose "Constructed primary named payload URL: $download_link" + + # Add link info to arrays + download_links+=($download_link) + specific_versions+=($specific_version) + effective_versions+=($effective_version) + link_types+=("primary") + + legacy_download_link="$(construct_legacy_download_link "$feed" "$channel" "$normalized_architecture" "$specific_version")" || valid_legacy_download_link=false + + if [ "$valid_legacy_download_link" = true ]; then + say_verbose "Constructed legacy named payload URL: $legacy_download_link" + + download_links+=($legacy_download_link) + specific_versions+=($specific_version) + effective_versions+=($effective_version) + link_types+=("legacy") + else + legacy_download_link="" + say_verbose "Cound not construct a legacy_download_link; omitting..." + fi + + # Check if the SDK version is already installed. + if [[ "$dry_run" != true ]] && is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then + say "$asset_name with version '$effective_version' is already installed." + exit 0 + fi +} + +print_dry_run() { + + say "Payload URLs:" + + for link_index in "${!download_links[@]}" + do + say "URL #$link_index - ${link_types[$link_index]}: ${download_links[$link_index]}" + done + + resolved_version=${specific_versions[0]} + repeatable_command="./$script_name --version "\""$resolved_version"\"" --install-dir "\""$install_root"\"" --architecture "\""$normalized_architecture"\"" --os "\""$normalized_os"\""" + + if [ ! -z "$normalized_quality" ]; then + repeatable_command+=" --quality "\""$normalized_quality"\""" + fi + + if [[ "$runtime" == "dotnet" ]]; then + repeatable_command+=" --runtime "\""dotnet"\""" + elif [[ "$runtime" == "aspnetcore" ]]; then + repeatable_command+=" --runtime "\""aspnetcore"\""" + fi + + repeatable_command+="$non_dynamic_parameters" + + if [ -n "$feed_credential" ]; then + repeatable_command+=" --feed-credential "\"""\""" + fi + + say "Repeatable invocation: $repeatable_command" +} + +calculate_vars() { + eval $invocation + + script_name=$(basename "$0") + normalized_architecture="$(get_normalized_architecture_from_architecture "$architecture")" + say_verbose "Normalized architecture: '$normalized_architecture'." + normalized_os="$(get_normalized_os "$user_defined_os")" + say_verbose "Normalized OS: '$normalized_os'." + normalized_quality="$(get_normalized_quality "$quality")" + say_verbose "Normalized quality: '$normalized_quality'." + normalized_channel="$(get_normalized_channel "$channel")" + say_verbose "Normalized channel: '$normalized_channel'." + normalized_product="$(get_normalized_product "$runtime")" + say_verbose "Normalized product: '$normalized_product'." + install_root="$(resolve_installation_path "$install_dir")" + say_verbose "InstallRoot: '$install_root'." + + normalized_architecture="$(get_normalized_architecture_for_specific_sdk_version "$version" "$normalized_channel" "$normalized_architecture")" + + if [[ "$runtime" == "dotnet" ]]; then + asset_relative_path="shared/Microsoft.NETCore.App" + asset_name=".NET Core Runtime" + elif [[ "$runtime" == "aspnetcore" ]]; then + asset_relative_path="shared/Microsoft.AspNetCore.App" + asset_name="ASP.NET Core Runtime" + elif [ -z "$runtime" ]; then + asset_relative_path="sdk" + asset_name=".NET Core SDK" + fi + + get_feeds_to_use +} + +install_dotnet() { + eval $invocation + local download_failed=false + local download_completed=false + + mkdir -p "$install_root" + zip_path="$(mktemp "$temporary_file_template")" + say_verbose "Zip path: $zip_path" + + for link_index in "${!download_links[@]}" + do + download_link="${download_links[$link_index]}" + specific_version="${specific_versions[$link_index]}" + effective_version="${effective_versions[$link_index]}" + link_type="${link_types[$link_index]}" + + say "Attempting to download using $link_type link $download_link" + + # The download function will set variables $http_code and $download_error_msg in case of failure. + download_failed=false + download "$download_link" "$zip_path" 2>&1 || download_failed=true + + if [ "$download_failed" = true ]; then + case $http_code in + 404) + say "The resource at $link_type link '$download_link' is not available." + ;; + *) + say "Failed to download $link_type link '$download_link': $download_error_msg" + ;; + esac + rm -f "$zip_path" 2>&1 && say_verbose "Temporary zip file $zip_path was removed" + else + download_completed=true + break + fi + done + + if [[ "$download_completed" == false ]]; then + say_err "Could not find \`$asset_name\` with version = $specific_version" + say_err "Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support" + return 1 + fi + + say "Extracting zip from $download_link" + extract_dotnet_package "$zip_path" "$install_root" || return 1 + + # Check if the SDK version is installed; if not, fail the installation. + # if the version contains "RTM" or "servicing"; check if a 'release-type' SDK version is installed. + if [[ $specific_version == *"rtm"* || $specific_version == *"servicing"* ]]; then + IFS='-' + read -ra verArr <<< "$specific_version" + release_version="${verArr[0]}" + unset IFS; + say_verbose "Checking installation: version = $release_version" + if is_dotnet_package_installed "$install_root" "$asset_relative_path" "$release_version"; then + say "Installed version is $effective_version" + return 0 + fi + fi + + # Check if the standard SDK version is installed. + say_verbose "Checking installation: version = $effective_version" + if is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then + say "Installed version is $effective_version" + return 0 + fi + + # Version verification failed. More likely something is wrong either with the downloaded content or with the verification algorithm. + say_err "Failed to verify the version of installed \`$asset_name\`.\nInstallation source: $download_link.\nInstallation location: $install_root.\nReport the bug at https://github.com/dotnet/install-scripts/issues." + say_err "\`$asset_name\` with version = $effective_version failed to install with an error." + return 1 +} + +args=("$@") + +local_version_file_relative_path="/.version" +bin_folder_relative_path="" +temporary_file_template="${TMPDIR:-/tmp}/dotnet.XXXXXXXXX" + +channel="LTS" +version="Latest" +json_file="" +install_dir="" +architecture="" +dry_run=false +no_path=false +no_cdn=false +azure_feed="" +uncached_feed="" +feed_credential="" +verbose=false +runtime="" +runtime_id="" +quality="" +internal=false +override_non_versioned_files=true +non_dynamic_parameters="" +user_defined_os="" + +while [ $# -ne 0 ] +do + name="$1" + case "$name" in + -c|--channel|-[Cc]hannel) + shift + channel="$1" + ;; + -v|--version|-[Vv]ersion) + shift + version="$1" + ;; + -q|--quality|-[Qq]uality) + shift + quality="$1" + ;; + --internal|-[Ii]nternal) + internal=true + non_dynamic_parameters+=" $name" + ;; + -i|--install-dir|-[Ii]nstall[Dd]ir) + shift + install_dir="$1" + ;; + --arch|--architecture|-[Aa]rch|-[Aa]rchitecture) + shift + architecture="$1" + ;; + --os|-[Oo][SS]) + shift + user_defined_os="$1" + ;; + --shared-runtime|-[Ss]hared[Rr]untime) + say_warning "The --shared-runtime flag is obsolete and may be removed in a future version of this script. The recommended usage is to specify '--runtime dotnet'." + if [ -z "$runtime" ]; then + runtime="dotnet" + fi + ;; + --runtime|-[Rr]untime) + shift + runtime="$1" + if [[ "$runtime" != "dotnet" ]] && [[ "$runtime" != "aspnetcore" ]]; then + say_err "Unsupported value for --runtime: '$1'. Valid values are 'dotnet' and 'aspnetcore'." + if [[ "$runtime" == "windowsdesktop" ]]; then + say_err "WindowsDesktop archives are manufactured for Windows platforms only." + fi + exit 1 + fi + ;; + --dry-run|-[Dd]ry[Rr]un) + dry_run=true + ;; + --no-path|-[Nn]o[Pp]ath) + no_path=true + non_dynamic_parameters+=" $name" + ;; + --verbose|-[Vv]erbose) + verbose=true + non_dynamic_parameters+=" $name" + ;; + --no-cdn|-[Nn]o[Cc]dn) + no_cdn=true + non_dynamic_parameters+=" $name" + ;; + --azure-feed|-[Aa]zure[Ff]eed) + shift + azure_feed="$1" + non_dynamic_parameters+=" $name "\""$1"\""" + ;; + --uncached-feed|-[Uu]ncached[Ff]eed) + shift + uncached_feed="$1" + non_dynamic_parameters+=" $name "\""$1"\""" + ;; + --feed-credential|-[Ff]eed[Cc]redential) + shift + feed_credential="$1" + #feed_credential should start with "?", for it to be added to the end of the link. + #adding "?" at the beginning of the feed_credential if needed. + [[ -z "$(echo $feed_credential)" ]] || [[ $feed_credential == \?* ]] || feed_credential="?$feed_credential" + ;; + --runtime-id|-[Rr]untime[Ii]d) + shift + runtime_id="$1" + non_dynamic_parameters+=" $name "\""$1"\""" + say_warning "Use of --runtime-id is obsolete and should be limited to the versions below 2.1. To override architecture, use --architecture option instead. To override OS, use --os option instead." + ;; + --jsonfile|-[Jj][Ss]on[Ff]ile) + shift + json_file="$1" + ;; + --skip-non-versioned-files|-[Ss]kip[Nn]on[Vv]ersioned[Ff]iles) + override_non_versioned_files=false + non_dynamic_parameters+=" $name" + ;; + -?|--?|-h|--help|-[Hh]elp) + script_name="$(basename "$0")" + echo ".NET Tools Installer" + echo "Usage: $script_name [-c|--channel ] [-v|--version ] [-p|--prefix ]" + echo " $script_name -h|-?|--help" + echo "" + echo "$script_name is a simple command line interface for obtaining dotnet cli." + echo " Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:" + echo " - The SDK needs to be installed without user interaction and without admin rights." + echo " - The SDK installation doesn't need to persist across multiple CI runs." + echo " To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer." + echo "" + echo "Options:" + echo " -c,--channel Download from the channel specified, Defaults to \`$channel\`." + echo " -Channel" + echo " Possible values:" + echo " - STS - the most recent Standard Term Support release" + echo " - LTS - the most recent Long Term Support release" + echo " - 2-part version in a format A.B - represents a specific release" + echo " examples: 2.0; 1.0" + echo " - 3-part version in a format A.B.Cxx - represents a specific SDK release" + echo " examples: 5.0.1xx, 5.0.2xx." + echo " Supported since 5.0 release" + echo " Warning: Value 'Current' is deprecated for the Channel parameter. Use 'STS' instead." + echo " Note: The version parameter overrides the channel parameter when any version other than 'latest' is used." + echo " -v,--version Use specific VERSION, Defaults to \`$version\`." + echo " -Version" + echo " Possible values:" + echo " - latest - the latest build on specific channel" + echo " - 3-part version in a format A.B.C - represents specific version of build" + echo " examples: 2.0.0-preview2-006120; 1.1.0" + echo " -q,--quality Download the latest build of specified quality in the channel." + echo " -Quality" + echo " The possible values are: daily, signed, validated, preview, GA." + echo " Works only in combination with channel. Not applicable for STS and LTS channels and will be ignored if those channels are used." + echo " For SDK use channel in A.B.Cxx format. Using quality for SDK together with channel in A.B format is not supported." + echo " Supported since 5.0 release." + echo " Note: The version parameter overrides the channel parameter when any version other than 'latest' is used, and therefore overrides the quality." + echo " --internal,-Internal Download internal builds. Requires providing credentials via --feed-credential parameter." + echo " --feed-credential Token to access Azure feed. Used as a query string to append to the Azure feed." + echo " -FeedCredential This parameter typically is not specified." + echo " -i,--install-dir Install under specified location (see Install Location below)" + echo " -InstallDir" + echo " --architecture Architecture of dotnet binaries to be installed, Defaults to \`$architecture\`." + echo " --arch,-Architecture,-Arch" + echo " Possible values: x64, arm, arm64, s390x and ppc64le" + echo " --os Specifies operating system to be used when selecting the installer." + echo " Overrides the OS determination approach used by the script. Supported values: osx, linux, linux-musl, freebsd, rhel.6." + echo " In case any other value is provided, the platform will be determined by the script based on machine configuration." + echo " Not supported for legacy links. Use --runtime-id to specify platform for legacy links." + echo " Refer to: https://aka.ms/dotnet-os-lifecycle for more information." + echo " --runtime Installs a shared runtime only, without the SDK." + echo " -Runtime" + echo " Possible values:" + echo " - dotnet - the Microsoft.NETCore.App shared runtime" + echo " - aspnetcore - the Microsoft.AspNetCore.App shared runtime" + echo " --dry-run,-DryRun Do not perform installation. Display download link." + echo " --no-path, -NoPath Do not set PATH for the current process." + echo " --verbose,-Verbose Display diagnostics information." + echo " --azure-feed,-AzureFeed For internal use only." + echo " Allows using a different storage to download SDK archives from." + echo " This parameter is only used if --no-cdn is false." + echo " --uncached-feed,-UncachedFeed For internal use only." + echo " Allows using a different storage to download SDK archives from." + echo " This parameter is only used if --no-cdn is true." + echo " --skip-non-versioned-files Skips non-versioned files if they already exist, such as the dotnet executable." + echo " -SkipNonVersionedFiles" + echo " --no-cdn,-NoCdn Disable downloading from the Azure CDN, and use the uncached feed directly." + echo " --jsonfile Determines the SDK version from a user specified global.json file." + echo " Note: global.json must have a value for 'SDK:Version'" + echo " -?,--?,-h,--help,-Help Shows this help message" + echo "" + echo "Install Location:" + echo " Location is chosen in following order:" + echo " - --install-dir option" + echo " - Environmental variable DOTNET_INSTALL_DIR" + echo " - $HOME/.dotnet" + exit 0 + ;; + *) + say_err "Unknown argument \`$name\`" + exit 1 + ;; + esac + + shift +done + +say_verbose "Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:" +say_verbose "- The SDK needs to be installed without user interaction and without admin rights." +say_verbose "- The SDK installation doesn't need to persist across multiple CI runs." +say_verbose "To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer.\n" + +if [ "$internal" = true ] && [ -z "$(echo $feed_credential)" ]; then + message="Provide credentials via --feed-credential parameter." + if [ "$dry_run" = true ]; then + say_warning "$message" + else + say_err "$message" + exit 1 + fi +fi + +check_min_reqs +calculate_vars +# generate_regular_links call below will 'exit' if the determined version is already installed. +generate_download_links + +if [[ "$dry_run" = true ]]; then + print_dry_run + exit 0 +fi + +install_dotnet + +bin_path="$(get_absolute_path "$(combine_paths "$install_root" "$bin_folder_relative_path")")" +if [ "$no_path" = false ]; then + say "Adding to current process PATH: \`$bin_path\`. Note: This change will be visible only when sourcing script." + export PATH="$bin_path":"$PATH" +else + say "Binaries of dotnet can be found in $bin_path" +fi + +say "Note that the script does not resolve dependencies during installation." +say "To check the list of dependencies, go to https://learn.microsoft.com/dotnet/core/install, select your operating system and check the \"Dependencies\" section." +say "Installation finished successfully." diff --git a/test/dotnet/dotnet_env.sh b/test/dotnet/dotnet_env.sh new file mode 100644 index 0000000..37b14ea --- /dev/null +++ b/test/dotnet/dotnet_env.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export DOTNET_NOLOGO=true +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true +export DOTNET_GENERATE_ASPNET_CERTIFICATE=false \ No newline at end of file diff --git a/test/dotnet/dotnet_helpers.sh b/test/dotnet/dotnet_helpers.sh new file mode 100644 index 0000000..6c833b4 --- /dev/null +++ b/test/dotnet/dotnet_helpers.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# Prints the latest dotnet version in the specified channel +# Usage: fetch_latest_version_in_channel [] +# Example: fetch_latest_version_in_channel "LTS" +# Example: fetch_latest_version_in_channel "6.0" "dotnet" +# Example: fetch_latest_version_in_channel "6.0" "aspnetcore" +fetch_latest_version_in_channel() { + local channel="$1" + local runtime="$2" + if [ "$runtime" = "dotnet" ]; then + wget -qO- "https://dotnetcli.azureedge.net/dotnet/Runtime/$channel/latest.version" + elif [ "$runtime" = "aspnetcore" ]; then + wget -qO- "https://dotnetcli.azureedge.net/dotnet/aspnetcore/Runtime/$channel/latest.version" + else + wget -qO- "https://dotnetcli.azureedge.net/dotnet/Sdk/$channel/latest.version" + fi + +} + +# Prints the latest dotnet version +# Usage: fetch_latest_version [] +# Example: fetch_latest_version +# Example: fetch_latest_version "dotnet" +# Example: fetch_latest_version "aspnetcore" +fetch_latest_version() { + local runtime="$1" + local sts_version + local lts_version + sts_version=$(fetch_latest_version_in_channel "STS" "$runtime") + lts_version=$(fetch_latest_version_in_channel "LTS" "$runtime") + if [[ "$sts_version" > "$lts_version" ]]; then + echo "$sts_version" + else + echo "$lts_version" + fi +} + +# Asserts that the specified .NET SDK version is installed +# Returns a non-zero exit code if the check fails +# Usage: is_dotnet_sdk_version_installed +# Example: is_dotnet_sdk_version_installed "6.0" +# Example: is_dotnet_sdk_version_installed "6.0.412" +is_dotnet_sdk_version_installed() { + local expected="$1" + dotnet --list-sdks | grep --fixed-strings --silent "$expected" + return $? +} + + +# Asserts that the specified .NET Runtime version is installed +# Returns a non-zero exit code if the check fails +# Usage: is_dotnet_runtime_version_installed +# Example: is_dotnet_runtime_version_installed "6.0" +# Example: is_dotnet_runtime_version_installed "6.0.412" +is_dotnet_runtime_version_installed() { + local expected="$1" + dotnet --list-runtimes | grep --fixed-strings --silent "Microsoft.NETCore.App $expected" + return $? +} + +# Asserts that the specified ASP.NET Core Runtime version is installed +# Returns a non-zero exit code if the check fails +# Usage: is_aspnetcore_runtime_version_installed +# Example: is_aspnetcore_runtime_version_installed "6.0" +# Example: is_aspnetcore_runtime_version_installed "6.0.412" +is_aspnetcore_runtime_version_installed() { + local expected="$1" + dotnet --list-runtimes | grep --fixed-strings --silent "Microsoft.AspNetCore.App $expected" + return $? +} \ No newline at end of file diff --git a/test/dotnet/install_additional_dotnet.sh b/test/dotnet/install_additional_dotnet.sh deleted file mode 100644 index e69c7dc..0000000 --- a/test/dotnet/install_additional_dotnet.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -set -e - -# Optional: Import test library -source dev-container-features-test-lib - -check "dotnet version 6.0.301 installed as default" bash -c "dotnet --version | grep 6.0.301" -check "dotnet version 5.0 installed" bash -c "ls -l /usr/local/dotnet | grep 5.0" -check "dotnet version 3.1.420 installed" bash -c "ls -l /usr/local/dotnet | grep 3.1.420" - -# Verify current symlink exists and works -check "current link dotnet" /usr/local/dotnet/current/dotnet --info -check "current link sdk" ls -l /usr/local/dotnet/current/sdk - -# Report result -reportResults diff --git a/test/dotnet/install_aspnetcore_runtime_only.sh b/test/dotnet/install_aspnetcore_runtime_only.sh new file mode 100644 index 0000000..35bd540 --- /dev/null +++ b/test/dotnet/install_aspnetcore_runtime_only.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -e + +# Optional: Import test library bundled with the devcontainer CLI +# See https://github.com/devcontainers/cli/blob/HEAD/docs/features/test.md#dev-container-features-test-lib +# Provides the 'check' and 'reportResults' commands. +source dev-container-features-test-lib + +# Feature-specific tests +# The 'check' command comes from the dev-container-features-test-lib. Syntax is... +# check