Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/conda/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "conda",
"version": "2.0.1",
"version": "2.0.2",
"name": "Conda",
"description": "A cross-platform, language-agnostic binary package manager",
"documentationURL": "https://github.com/devcontainers/features/tree/main/src/conda",
Expand Down
45 changes: 34 additions & 11 deletions src/conda/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#-------------------------------------------------------------------------------------------------------------

VERSION=${VERSION:-"latest"}
ADD_CONDA_FORGE=$ADDCONDAFORGE
ADD_CONDA_FORGE=${ADDCONDAFORGE:-"false"}

USERNAME="${USERNAME:-"${_REMOTE_USER:-"automatic"}"}"
UPDATE_RC="true"
Expand Down Expand Up @@ -70,15 +70,10 @@ sudo_if() {
if [ "$(id -u)" -eq 0 ] && [ "$USERNAME" != "root" ]; then
su - "$USERNAME" -c "$COMMAND"
else
$COMMAND
"$@"
fi
}

install_user_package() {
PACKAGE="$1"
sudo_if "${CONDA_DIR}/bin/python3" -m pip install --user --upgrade "$PACKAGE"
}

accept_anaconda_tos_if_needed() {
if ! "${CONDA_DIR}/bin/conda" tos --help > /dev/null 2>&1; then
return 0
Expand All @@ -95,8 +90,21 @@ accept_anaconda_tos_if_needed() {

clean_conda_cache() {
"${CONDA_DIR}/bin/conda" clean --all --yes

# pip upgrades can leave older, extracted Conda packages behind.
for pkg_path in "${CONDA_DIR}"/pkgs/pip-[0-9]*; do
[ -e "${pkg_path}" ] || continue
pkg_name="$(basename "${pkg_path}")"
pkg_version="${pkg_name#pip-}"
pkg_version="${pkg_version%%-*}"
greater_version="$(printf '%s\n%s\n' "${pkg_version}" "26.1" | sort -V | tail -1)"
if [ "${pkg_version}" != "${greater_version}" ]; then
rm -rf "${pkg_path}"
fi
done

find "${CONDA_DIR}" -type f -name '*.pyc' -delete
find "${CONDA_DIR}" -type d -name '__pycache__' -exec rm -rf {} +
find "${CONDA_DIR}" -type d -name '__pycache__' -prune -exec rm -rf {} +
rm -rf "${CONDA_DIR}/pkgs/cache" /root/.cache/pip
if [ "${USERNAME}" != "root" ]; then
rm -rf "/home/${USERNAME}/.cache/pip"
Expand Down Expand Up @@ -155,12 +163,27 @@ if ! conda --version &> /dev/null ; then

chown -R "${USERNAME}:conda" "${CONDA_DIR}"
chmod -R g+r+w "${CONDA_DIR}"

find "${CONDA_DIR}" -type d -print0 | xargs -n 1 -0 chmod g+s

clean_conda_cache
find "${CONDA_DIR}" -type d -print0 | xargs -n 1 -0 chmod g+s
fi

# Apply Universal's security patches before committing the Conda installation layer.
# https://github.com/advisories/GHSA-537c-gmf6-5ccf
# https://nvd.nist.gov/vuln/detail/CVE-2025-6176
sudo_if "${CONDA_DIR}/bin/conda" install -y -c defaults \
cryptography=50.0.0 \
pyopenssl=26.4.0 \
openssl=3.5.8 \
brotli=1.2.0

# Upgrade pip after Conda so the solver cannot downgrade the patched pip version.
# https://github.com/advisories/GHSA-jp4c-xjxw-mgf9
# https://github.com/advisories/GHSA-mf9w-mj56-hr94
sudo_if "${CONDA_DIR}/bin/python3" -m pip install --upgrade --no-cache-dir pip
sudo_if "${CONDA_DIR}/bin/python3" -m pip install --upgrade --no-cache-dir python-dotenv==1.2.2

clean_conda_cache

# Display a notice on conda when not running in GitHub Codespaces
mkdir -p /usr/local/etc/vscode-dev-containers
cat << 'EOF' > /usr/local/etc/vscode-dev-containers/conda-notice.txt
Expand Down
10 changes: 10 additions & 0 deletions test/conda/install_conda_patched_packages.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash

set -e

source dev-container-features-test-lib

check "running as vscode" test "$(id -un)" = "vscode"
source "$(dirname "$0")/patched_packages_common.sh"

reportResults
10 changes: 10 additions & 0 deletions test/conda/install_conda_patched_packages_with_codespace.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash

set -e

source dev-container-features-test-lib

check "running as codespace" test "$(id -un)" = "codespace"
source "$(dirname "$0")/patched_packages_common.sh"

reportResults
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FROM ubuntu:noble

RUN if id ubuntu > /dev/null 2>&1; then userdel -f -r ubuntu; fi
10 changes: 10 additions & 0 deletions test/conda/install_conda_patched_packages_with_root.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash

set -e

source dev-container-features-test-lib

check "running as root" test "$(id -u)" = "0"
source "$(dirname "$0")/patched_packages_common.sh"

reportResults
60 changes: 60 additions & 0 deletions test/conda/patched_packages_common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/bin/bash

# Inspect cleanup before running Conda or importing packages can recreate caches.
cache_python=(/opt/conda/bin/python3 -B -)
if [ "$(id -u)" -ne 0 ]; then
cache_python=(sudo -n "${cache_python[@]}")
fi
check "installation caches removed" "${cache_python[@]}" "$HOME" <<'PY'
import sys
from pathlib import Path

from packaging.version import Version

conda_dir = Path("/opt/conda")
leftovers = list(conda_dir.rglob("*.pyc"))
leftovers.extend(conda_dir.rglob("__pycache__"))
for package in (conda_dir / "pkgs").glob("pip-[0-9]*"):
version = package.name.removeprefix("pip-").split("-", 1)[0]
if Version(version) < Version("26.1"):
leftovers.append(package)
for cache in (
conda_dir / "pkgs/cache",
Path("/root/.cache/pip"),
Path(sys.argv[1]) / ".cache/pip",
):
if cache.exists():
leftovers.append(cache)
assert not leftovers, f"Installation cache leftovers: {leftovers}"
PY

check "patched packages installed in base environment" env PYTHONNOUSERSITE=1 /opt/conda/bin/python3 -B - <<'PY'
import json
import sys
from importlib.metadata import version
from pathlib import Path

from packaging.version import Version

assert sys.prefix == "/opt/conda", f"Unexpected Python prefix: {sys.prefix}"
packages = {}
for metadata in Path("/opt/conda/conda-meta").glob("*.json"):
package = json.loads(metadata.read_text())
packages[package["name"]] = package["version"]

for name, expected in {
"cryptography": "50.0.0",
"pyopenssl": "26.4.0",
"openssl": "3.5.8",
"brotli": "1.2.0",
}.items():
actual = packages.get(name)
assert actual == expected, f"{name}: expected {expected}, got {actual}"

assert Version(version("pip")) >= Version("26.1"), f"Unpatched pip: {version('pip')}"
assert version("python-dotenv") == "1.2.2", f"Unexpected python-dotenv: {version('python-dotenv')}"
PY

check "base environment writable by runtime user" test -w /opt/conda/conda-meta
check "conda still works" conda --version
check "pip still works" /opt/conda/bin/python3 -m pip --version
28 changes: 28 additions & 0 deletions test/conda/scenarios.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,32 @@
{
"install_conda_patched_packages_with_codespace": {
"build": {
"dockerfile": "Dockerfile"
},
"containerUser": "codespace",
"remoteUser": "codespace",
"features": {
"common-utils": {
"username": "codespace",
"userUid": "1000",
"userGid": "1000"
},
"conda": {}
}
},
"install_conda_patched_packages": {
"image": "mcr.microsoft.com/devcontainers/base:noble",
"remoteUser": "vscode",
"features": {
"conda": {}
}
},
"install_conda_patched_packages_with_root": {
"image": "ubuntu:noble",
"features": {
"conda": {}
}
},
"install_conda": {
"image": "mcr.microsoft.com/devcontainers/base:noble",
"features": {
Expand Down
Loading