Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[python] - issue #1172 - provide switching versions using update-alternatives #1260

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions src/python/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -766,15 +766,19 @@ esac

check_packages ${REQUIRED_PKGS}

# Function to get the major version from a SemVer string
get_major_version() {
local version="$1"
echo "$version" | cut -d '.' -f 1
}

# Install Python from source if needed
if [ "${PYTHON_VERSION}" != "none" ]; then
if ! cat /etc/group | grep -e "^python:" > /dev/null 2>&1; then
groupadd -r python
fi
usermod -a -G python "${USERNAME}"

CURRENT_PATH="${PYTHON_INSTALL_PATH}/current"

install_python ${PYTHON_VERSION}

# Additional python versions to be installed but not be set as default.
Expand All @@ -783,9 +787,27 @@ if [ "${PYTHON_VERSION}" != "none" ]; then
OLDIFS=$IFS
IFS=","
read -a additional_versions <<< "$ADDITIONAL_VERSIONS"
for version in "${additional_versions[@]}"; do
major_version=$(get_major_version ${VERSION})
if type apt-get > /dev/null 2>&1; then
# Debian/Ubuntu: Use update-alternatives
update-alternatives --install ${CURRENT_PATH} python${major_version} ${PYTHON_INSTALL_PATH}/${VERSION} $((${#additional_versions[@]}+1))
update-alternatives --set python${major_version} ${PYTHON_INSTALL_PATH}/${VERSION}
elif type dnf > /dev/null 2>&1 || type yum > /dev/null 2>&1 || type microdnf > /dev/null 2>&1; then
# Fedora/RHEL/CentOS: Use alternatives
alternatives --install ${CURRENT_PATH} python${major_version} ${PYTHON_INSTALL_PATH}/${VERSION} $((${#additional_versions[@]}+1))
alternatives --set python${major_version} ${PYTHON_INSTALL_PATH}/${VERSION}
fi
for i in "${!additional_versions[@]}"; do
version=${additional_versions[$i]}
OVERRIDE_DEFAULT_VERSION="false"
install_python $version
if type apt-get > /dev/null 2>&1; then
# Debian/Ubuntu: Use update-alternatives
update-alternatives --install ${CURRENT_PATH} python${major_version} ${PYTHON_INSTALL_PATH}/${VERSION} $((${i}+1))
elif type dnf > /dev/null 2>&1 || type yum > /dev/null 2>&1 || type microdnf > /dev/null 2>&1; then
# Fedora/RHEL/CentOS: Use alternatives
alternatives --install ${CURRENT_PATH} python${major_version} ${PYTHON_INSTALL_PATH}/${VERSION} $((${i}+1))
fi
done
INSTALL_PATH="${OLD_INSTALL_PATH}"
IFS=$OLDIFS
Expand Down
103 changes: 103 additions & 0 deletions test/python/alternatives_switchable_versions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/bin/bash

set -e

# Optional: Import test library
source dev-container-features-test-lib

check "python version 3.11 installed as default" bash -c "python --version | grep 3.11"
check "python3 version 3.11 installed as default" bash -c "python3 --version | grep 3.11"
check "python version 3.10.5 installed" bash -c "ls -l /usr/local/python | grep 3.10.5"
check "python version 3.8 installed" bash -c "ls -l /usr/local/python | grep 3.8"
check "python version 3.9.13 installed" bash -c "ls -l /usr/local/python | grep 3.9.13"

# Check paths in settings
check "current symlink is correct" bash -c "which python | grep /usr/local/python/current/bin/python"
check "current symlink works" /usr/local/python/current/bin/python --version

# check alternatives command
check_version_switch() {
if type apt-get > /dev/null 2>&1; then
PYTHON_ALTERNATIVES=$(update-alternatives --query python3 | grep -E 'Alternative:|Priority:')
STYLE="debian"
elif type dnf > /dev/null 2>&1 || type yum > /dev/null 2>&1 || type microdnf > /dev/null 2>&1; then
PYTHON_ALTERNATIVES=$(alternatives --display python3 | grep " - priority")
STYLE="fedora"
else
echo "No supported package manager found."
exit 1
fi

AVAILABLE_VERSIONS=()
INDEX=1
echo "Available Python versions:"
if [ "${STYLE}" = "debian" ]; then
while read -r alt && read -r pri; do
PATH=${alt#Alternative: } # Extract only the path
PRIORITY=${pri#Priority: } # Extract only the priority number
TEMP_VERSIONS+=("${PRIORITY} ${PATH}")
echo "$INDEX) $PATH (Priority: $PRIORITY)"
((INDEX++))
done <<< "${PYTHON_ALTERNATIVES}"
elif [ "${STYLE}" = "fedora" ]; then
export PATH="/usr/bin:$PATH"
# Fedora/RHEL output: one line per alternative in the format:
while IFS= read -r line; do
# Split using " - priority " as a delimiter.
PATH=$(/usr/bin/awk -F' - priority ' '{print $1}' <<< "$line" | /usr/bin/xargs /bin/echo)
PRIORITY=$(/usr/bin/awk -F' - priority ' '{print $2}' <<< "$line" | /usr/bin/xargs /bin/echo)
TEMP_VERSIONS+=("${PRIORITY} ${PATH}")
echo "$INDEX) $PATH (Priority: $PRIORITY_VALUE)"
((INDEX++))
done <<< "${PYTHON_ALTERNATIVES}"
fi

export PATH="/usr/bin:$PATH"
# Sort by priority (numerically ascending)
IFS=$'\n' TEMP_VERSIONS=($(sort -n <<<"${TEMP_VERSIONS[*]}"))
unset IFS

# Populate AVAILABLE_VERSIONS from sorted data
AVAILABLE_VERSIONS=()
INDEX=1
echo -e "\nAvailable Python versions (Sorted in asc order of priority):"
for ENTRY in "${TEMP_VERSIONS[@]}"; do
PRIORITY=${ENTRY%% *} # Extract priority (first part before space)
PATH=${ENTRY#* } # Extract path (everything after first space)
AVAILABLE_VERSIONS+=("${PATH}")
echo "$INDEX) $PATH (Priority: $PRIORITY)"
((INDEX++))
done

echo -e "\nAvailable Versions Count: ${#AVAILABLE_VERSIONS[@]}"
# Ensure at least 4 alternatives exist
if [ "${#AVAILABLE_VERSIONS[@]}" -lt 4 ]; then
echo "Error: Less than 4 Python versions registered in update-alternatives."
exit 1
fi

export PATH="/usr/bin:$PATH"
echo -e "\nSwitching to different versions using update-alternatives --set command...\n"
for CHOICE in {1..4}; do
SELECTED_VERSION="${AVAILABLE_VERSIONS[$((CHOICE - 1))]}"
echo "Switching to: ${SELECTED_VERSION}"
if command -v apt-get > /dev/null 2>&1; then
/usr/bin/update-alternatives --set python3 ${SELECTED_VERSION}
elif command -v dnf > /dev/null 2>&1 || command -v yum > /dev/null 2>&1 || command -v microdnf > /dev/null 2>&1; then
/usr/sbin/alternatives --set python3 ${SELECTED_VERSION}
fi
# Verify the switch
echo "Python version after switch:"
/usr/local/python/current/bin/python3 --version
/bin/sleep 1
echo -e "\n"
done
echo -e "Update-Alternatives --display: \n"
if type apt-get > /dev/null 2>&1; then
/usr/bin/update-alternatives --display python3
elif type dnf > /dev/null 2>&1 || type yum > /dev/null 2>&1 || type microdnf > /dev/null 2>&1; then
/usr/sbin/alternatives --display python3
fi
}

check "Version Switch With Update_Alternatives" check_version_switch
20 changes: 20 additions & 0 deletions test/python/scenarios.json
Original file line number Diff line number Diff line change
Expand Up @@ -255,5 +255,25 @@
"enableShared": true
}
}
},
"update_alternatives_switchable_versions": {
"image": "ubuntu:focal",
"features": {
"python": {
"version": "3.11",
"installTools": true,
"additionalVersions": "3.8,3.9.13,3.10.5"
}
}
},
"alternatives_switchable_versions": {
"image": "fedora",
"features": {
"python": {
"version": "3.11",
"installTools": true,
"additionalVersions": "3.8,3.9.13,3.10.5"
}
}
}
}
102 changes: 102 additions & 0 deletions test/python/update_alternatives_switchable_versions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/bin/bash

set -e

# Optional: Import test library
source dev-container-features-test-lib

check "python version 3.11 installed as default" bash -c "python --version | grep 3.11"
check "python3 version 3.11 installed as default" bash -c "python3 --version | grep 3.11"
check "python version 3.10.5 installed" bash -c "ls -l /usr/local/python | grep 3.10.5"
check "python version 3.8 installed" bash -c "ls -l /usr/local/python | grep 3.8"
check "python version 3.9.13 installed" bash -c "ls -l /usr/local/python | grep 3.9.13"

# Check paths in settings
check "current symlink is correct" bash -c "which python | grep /usr/local/python/current/bin/python"
check "current symlink works" /usr/local/python/current/bin/python --version

# check alternatives command
check_version_switch() {
if type apt-get > /dev/null 2>&1; then
PYTHON_ALTERNATIVES=$(update-alternatives --query python3 | grep -E 'Alternative:|Priority:')
STYLE="debian"
elif type dnf > /dev/null 2>&1 || type yum > /dev/null 2>&1 || type microdnf > /dev/null 2>&1; then
PYTHON_ALTERNATIVES=$(alternatives --display python3 | grep " - priority")
STYLE="fedora"
else
echo "No supported package manager found."
exit 1
fi
AVAILABLE_VERSIONS=()
INDEX=1
echo "Available Python versions:"
if [ "${STYLE}" = "debian" ]; then
while read -r alt && read -r pri; do
PATH=${alt#Alternative: } # Extract only the path
PRIORITY=${pri#Priority: } # Extract only the priority number
TEMP_VERSIONS+=("${PRIORITY} ${PATH}")
echo "$INDEX) $PATH (Priority: $PRIORITY)"
((INDEX++))
done <<< "${PYTHON_ALTERNATIVES}"
elif [ "${STYLE}" = "fedora" ]; then
export PATH="/usr/bin:$PATH"
# Fedora/RHEL output: one line per alternative in the format:
while IFS= read -r line; do
# Split using " - priority " as a delimiter.
PATH=$(/usr/bin/awk -F' - priority ' '{print $1}' <<< "$line" | /usr/bin/xargs /bin/echo)
PRIORITY=$(/usr/bin/awk -F' - priority ' '{print $2}' <<< "$line" | /usr/bin/xargs /bin/echo)
TEMP_VERSIONS+=("${PRIORITY} ${PATH}")
echo "$INDEX) $PATH (Priority: $PRIORITY_VALUE)"
((INDEX++))
done <<< "${PYTHON_ALTERNATIVES}"
fi

export PATH="/usr/bin:$PATH"
# Sort by priority (numerically ascending)
IFS=$'\n' TEMP_VERSIONS=($(sort -n <<<"${TEMP_VERSIONS[*]}"))
unset IFS

# Populate AVAILABLE_VERSIONS from sorted data
AVAILABLE_VERSIONS=()
INDEX=1
echo -e "\nAvailable Python versions (Sorted in asc order of priority):"
for ENTRY in "${TEMP_VERSIONS[@]}"; do
PRIORITY=${ENTRY%% *} # Extract priority (first part before space)
PATH=${ENTRY#* } # Extract path (everything after first space)
AVAILABLE_VERSIONS+=("${PATH}")
echo "$INDEX) $PATH (Priority: $PRIORITY)"
((INDEX++))
done

echo -e "\nAvailable Versions Count: ${#AVAILABLE_VERSIONS[@]}\n"
# Ensure at least 4 alternatives exist
if [ "${#AVAILABLE_VERSIONS[@]}" -lt 4 ]; then
echo "Error: Less than 4 Python versions registered in update-alternatives."
exit 1
fi

export PATH="/usr/bin:$PATH"
echo -e "\nSwitching to different versions using update-alternatives --set command...\n"
for CHOICE in {1..4}; do
SELECTED_VERSION="${AVAILABLE_VERSIONS[$((CHOICE - 1))]}"
echo "Switching to: ${SELECTED_VERSION}"
if command -v apt-get > /dev/null 2>&1; then
/usr/bin/update-alternatives --set python3 ${SELECTED_VERSION}
elif command -v dnf > /dev/null 2>&1 || command -v yum > /dev/null 2>&1 || command -v microdnf > /dev/null 2>&1; then
/usr/sbin/alternatives --set python3 ${SELECTED_VERSION}
fi
# Verify the switch
echo "Python version after switch:"
/usr/local/python/current/bin/python3 --version
/bin/sleep 1
echo -e "\n"
done
echo -e "Update-Alternatives --display: \n"
if type apt-get > /dev/null 2>&1; then
/usr/bin/update-alternatives --display python3
elif type dnf > /dev/null 2>&1 || type yum > /dev/null 2>&1 || type microdnf > /dev/null 2>&1; then
/usr/sbin/alternatives --display python3
fi
}

check "Version Switch With Update_Alternatives" check_version_switch