mirror of
https://github.com/jupyterhub/the-littlest-jupyterhub.git
synced 2025-12-18 21:54:05 +08:00
Merge pull request #858 from minrk/update-base-env
Update base user environment to mambaforge 23.1.0-1 (Python 3.10)
This commit is contained in:
2
.github/workflows/integration-test.yaml
vendored
2
.github/workflows/integration-test.yaml
vendored
@@ -69,7 +69,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pytest --verbose --maxfail=2 --color=yes --durations=10 --capture=no \
|
pytest --verbose --maxfail=2 --color=yes --durations=10 --capture=no \
|
||||||
integration-tests/test_bootstrap.py
|
integration-tests/test_bootstrap.py
|
||||||
timeout-minutes: 15
|
timeout-minutes: 20
|
||||||
env:
|
env:
|
||||||
# integration-tests/test_bootstrap.py will build and start containers
|
# integration-tests/test_bootstrap.py will build and start containers
|
||||||
# based on this environment variable. This is similar to how
|
# based on this environment variable. This is similar to how
|
||||||
|
|||||||
1
.github/workflows/unit-test.yaml
vendored
1
.github/workflows/unit-test.yaml
vendored
@@ -59,6 +59,7 @@ jobs:
|
|||||||
apt-get update
|
apt-get update
|
||||||
apt-get install --yes \
|
apt-get install --yes \
|
||||||
python3-venv \
|
python3-venv \
|
||||||
|
bzip2 \
|
||||||
git
|
git
|
||||||
|
|
||||||
python3 -m venv /srv/venv
|
python3 -m venv /srv/venv
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
packaging
|
||||||
pytest
|
pytest
|
||||||
pytest-cov
|
pytest-cov
|
||||||
pytest-mock
|
pytest-mock
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ RUN export DEBIAN_FRONTEND=noninteractive \
|
|||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install --yes \
|
&& apt-get install --yes \
|
||||||
systemd \
|
systemd \
|
||||||
|
bzip2 \
|
||||||
curl \
|
curl \
|
||||||
git \
|
git \
|
||||||
sudo \
|
sudo \
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
Test conda commandline wrappers
|
Test conda commandline wrappers
|
||||||
"""
|
"""
|
||||||
from tljh import conda
|
from tljh import conda
|
||||||
|
from tljh import installer
|
||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -13,25 +14,13 @@ def prefix():
|
|||||||
"""
|
"""
|
||||||
Provide a temporary directory with a mambaforge conda environment
|
Provide a temporary directory with a mambaforge conda environment
|
||||||
"""
|
"""
|
||||||
# see https://github.com/conda-forge/miniforge/releases
|
machine = os.uname().machine
|
||||||
mambaforge_version = "4.10.3-7"
|
installer_url, checksum = installer._mambaforge_url()
|
||||||
if os.uname().machine == "aarch64":
|
|
||||||
installer_sha256 = (
|
|
||||||
"ac95f137b287b3408e4f67f07a284357b1119ee157373b788b34e770ef2392b2"
|
|
||||||
)
|
|
||||||
elif os.uname().machine == "x86_64":
|
|
||||||
installer_sha256 = (
|
|
||||||
"fc872522ec427fcab10167a93e802efaf251024b58cc27b084b915a9a73c4474"
|
|
||||||
)
|
|
||||||
installer_url = "https://github.com/conda-forge/miniforge/releases/download/{v}/Mambaforge-{v}-Linux-{arch}.sh".format(
|
|
||||||
v=mambaforge_version, arch=os.uname().machine
|
|
||||||
)
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
with conda.download_miniconda_installer(
|
with conda.download_miniconda_installer(
|
||||||
installer_url, installer_sha256
|
installer_url, checksum
|
||||||
) as installer_path:
|
) as installer_path:
|
||||||
conda.install_miniconda(installer_path, tmpdir)
|
conda.install_miniconda(installer_path, tmpdir)
|
||||||
conda.ensure_conda_packages(tmpdir, ["conda==4.10.3"])
|
|
||||||
yield tmpdir
|
yield tmpdir
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
Unit test functions in installer.py
|
Unit test functions in installer.py
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
from unittest import mock
|
||||||
|
from subprocess import run, PIPE
|
||||||
|
|
||||||
|
from packaging.version import parse as V
|
||||||
|
from packaging.specifiers import SpecifierSet
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from tljh import conda
|
||||||
from tljh import installer
|
from tljh import installer
|
||||||
from tljh.yaml import yaml
|
from tljh.yaml import yaml
|
||||||
|
|
||||||
@@ -36,3 +43,194 @@ def test_ensure_admins(tljh_dir, admins, expected_config):
|
|||||||
|
|
||||||
# verify the list was flattened
|
# verify the list was flattened
|
||||||
assert config["users"]["admin"] == expected_config
|
assert config["users"]["admin"] == expected_config
|
||||||
|
|
||||||
|
|
||||||
|
def setup_conda(distro, version, prefix):
|
||||||
|
"""Install mambaforge or miniconda in a prefix"""
|
||||||
|
if distro == "mambaforge":
|
||||||
|
installer_url, _ = installer._mambaforge_url(version)
|
||||||
|
elif distro == "miniforge":
|
||||||
|
installer_url, _ = installer._mambaforge_url(version)
|
||||||
|
installer_url = installer_url.replace("Mambaforge", "Miniforge3")
|
||||||
|
elif distro == "miniconda":
|
||||||
|
arch = os.uname().machine
|
||||||
|
installer_url = (
|
||||||
|
f"https://repo.anaconda.com/miniconda/Miniconda3-{version}-Linux-{arch}.sh"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"{distro=} must be 'miniconda' or 'mambaforge' or 'miniforge'"
|
||||||
|
)
|
||||||
|
with conda.download_miniconda_installer(installer_url, None) as installer_path:
|
||||||
|
conda.install_miniconda(installer_path, str(prefix))
|
||||||
|
# avoid auto-updating conda when we install other packages
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
str(prefix / "bin/conda"),
|
||||||
|
"config",
|
||||||
|
"--system",
|
||||||
|
"--set",
|
||||||
|
"auto_update_conda",
|
||||||
|
"false",
|
||||||
|
],
|
||||||
|
input="",
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def user_env_prefix(tmp_path):
|
||||||
|
user_env_prefix = tmp_path / "user_env"
|
||||||
|
with mock.patch.object(installer, "USER_ENV_PREFIX", str(user_env_prefix)):
|
||||||
|
yield user_env_prefix
|
||||||
|
|
||||||
|
|
||||||
|
def _specifier(version):
|
||||||
|
"""Convert version string to SpecifierSet
|
||||||
|
|
||||||
|
If just a version number, add == to make it a specifier
|
||||||
|
|
||||||
|
Any missing fields are replaced with .*
|
||||||
|
|
||||||
|
If it's already a specifier string, pass it directly to SpecifierSet
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
|
||||||
|
- 3.7 -> ==3.7.*
|
||||||
|
- 1.2.3 -> ==1.2.3
|
||||||
|
"""
|
||||||
|
if version[0].isdigit():
|
||||||
|
# it's a version number, not a specifier
|
||||||
|
if version.count(".") < 2:
|
||||||
|
# pad missing fields
|
||||||
|
version += ".*"
|
||||||
|
version = f"=={version}"
|
||||||
|
return SpecifierSet(version)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"distro, distro_version, expected_versions",
|
||||||
|
[
|
||||||
|
# No previous install, start fresh
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"python": "3.10.*",
|
||||||
|
"conda": "23.1.0",
|
||||||
|
"mamba": "1.4.1",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
# previous install, 1.0
|
||||||
|
(
|
||||||
|
"mambaforge",
|
||||||
|
"23.1.0-1",
|
||||||
|
{
|
||||||
|
"python": "3.10.*",
|
||||||
|
"conda": "23.1.0",
|
||||||
|
"mamba": "1.4.1",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
# 0.2 install, no upgrade needed
|
||||||
|
(
|
||||||
|
"mambaforge",
|
||||||
|
"4.10.3-7",
|
||||||
|
{
|
||||||
|
"conda": "4.10.3",
|
||||||
|
"mamba": "0.16.0",
|
||||||
|
"python": "3.9.*",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
# simulate missing mamba
|
||||||
|
# will be installed but not pinned
|
||||||
|
# to avoid conflicts
|
||||||
|
(
|
||||||
|
"miniforge",
|
||||||
|
"4.10.3-7",
|
||||||
|
{
|
||||||
|
"conda": "4.10.3",
|
||||||
|
"mamba": ">=1.1.0",
|
||||||
|
"python": "3.9.*",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
# too-old Python (3.7), abort
|
||||||
|
(
|
||||||
|
"miniconda",
|
||||||
|
"4.7.10",
|
||||||
|
ValueError,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_ensure_user_environment(
|
||||||
|
user_env_prefix,
|
||||||
|
distro,
|
||||||
|
distro_version,
|
||||||
|
expected_versions,
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
distro_version
|
||||||
|
and V(distro_version) < V("4.10.1")
|
||||||
|
and os.uname().machine == "aarch64"
|
||||||
|
):
|
||||||
|
pytest.skip(f"{distro} {distro_version} not available for aarch64")
|
||||||
|
canary_file = user_env_prefix / "test-file.txt"
|
||||||
|
canary_package = "types-backports_abc"
|
||||||
|
if distro:
|
||||||
|
setup_conda(distro, distro_version, user_env_prefix)
|
||||||
|
# install a noarch: python package that won't be used otherwise
|
||||||
|
# should depend on Python, so it will interact with possible upgrades
|
||||||
|
pkgs = [canary_package]
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
str(user_env_prefix / "bin/conda"),
|
||||||
|
"install",
|
||||||
|
"-S",
|
||||||
|
"-y",
|
||||||
|
"-c",
|
||||||
|
"conda-forge",
|
||||||
|
]
|
||||||
|
+ pkgs,
|
||||||
|
input="",
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# make a file not managed by conda, to check for wipeouts
|
||||||
|
with canary_file.open("w") as f:
|
||||||
|
f.write("I'm here\n")
|
||||||
|
|
||||||
|
if isinstance(expected_versions, type) and issubclass(expected_versions, Exception):
|
||||||
|
exc_class = expected_versions
|
||||||
|
with pytest.raises(exc_class):
|
||||||
|
installer.ensure_user_environment("")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
installer.ensure_user_environment("")
|
||||||
|
|
||||||
|
p = run(
|
||||||
|
[str(user_env_prefix / "bin/conda"), "list", "--json"],
|
||||||
|
stdout=PIPE,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
package_list = json.loads(p.stdout)
|
||||||
|
packages = {package["name"]: package for package in package_list}
|
||||||
|
|
||||||
|
if distro:
|
||||||
|
# make sure we didn't wipe out files
|
||||||
|
assert canary_file.exists()
|
||||||
|
# make sure we didn't delete the installed package
|
||||||
|
assert canary_package in packages
|
||||||
|
|
||||||
|
for pkg, version in expected_versions.items():
|
||||||
|
assert pkg in packages
|
||||||
|
assert V(packages[pkg]["version"]) in _specifier(version)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ensure_user_environment_no_clobber(user_env_prefix):
|
||||||
|
# don't clobber existing user-env dir if it's non-empty and not a conda install
|
||||||
|
user_env_prefix.mkdir()
|
||||||
|
canary_file = user_env_prefix / "test-file.txt"
|
||||||
|
with canary_file.open("w") as f:
|
||||||
|
pass
|
||||||
|
with pytest.raises(OSError):
|
||||||
|
installer.ensure_user_environment("")
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import subprocess
|
|||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import logging
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from distutils.version import LooseVersion as V
|
|
||||||
from tljh import utils
|
from tljh import utils
|
||||||
|
|
||||||
|
|
||||||
@@ -25,23 +28,21 @@ def sha256_file(fname):
|
|||||||
return hash_sha256.hexdigest()
|
return hash_sha256.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def check_miniconda_version(prefix, version):
|
def get_conda_package_versions(prefix):
|
||||||
"""
|
"""Get conda package versions, via `conda list --json`"""
|
||||||
Return true if a miniconda install with version exists at prefix
|
versions = {}
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
installed_version = (
|
out = subprocess.check_output(
|
||||||
subprocess.check_output(
|
[os.path.join(prefix, "bin", "conda"), "list", "--json"],
|
||||||
[os.path.join(prefix, "bin", "conda"), "-V"], stderr=subprocess.STDOUT
|
text=True,
|
||||||
)
|
|
||||||
.decode()
|
|
||||||
.strip()
|
|
||||||
.split()[1]
|
|
||||||
)
|
)
|
||||||
return V(installed_version) >= V(version)
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
# Conda doesn't exist
|
return versions
|
||||||
return False
|
|
||||||
|
packages = json.loads(out)
|
||||||
|
for package in packages:
|
||||||
|
versions[package["name"]] = package["version"]
|
||||||
|
return versions
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
@@ -53,14 +54,21 @@ def download_miniconda_installer(installer_url, sha256sum):
|
|||||||
of given version, verifies the sha256sum & provides path to it to the `with`
|
of given version, verifies the sha256sum & provides path to it to the `with`
|
||||||
block to run.
|
block to run.
|
||||||
"""
|
"""
|
||||||
with tempfile.NamedTemporaryFile("wb") as f:
|
logger = logging.getLogger("tljh")
|
||||||
f.write(requests.get(installer_url).content)
|
logger.info(f"Downloading conda installer {installer_url}")
|
||||||
|
with tempfile.NamedTemporaryFile("wb", suffix=".sh") as f:
|
||||||
|
tic = time.perf_counter()
|
||||||
|
r = requests.get(installer_url)
|
||||||
|
r.raise_for_status()
|
||||||
|
f.write(r.content)
|
||||||
# Remain in the NamedTemporaryFile context, but flush changes, see:
|
# Remain in the NamedTemporaryFile context, but flush changes, see:
|
||||||
# https://docs.python.org/3/library/os.html#os.fsync
|
# https://docs.python.org/3/library/os.html#os.fsync
|
||||||
f.flush()
|
f.flush()
|
||||||
os.fsync(f.fileno())
|
os.fsync(f.fileno())
|
||||||
|
t = time.perf_counter() - tic
|
||||||
|
logger.info(f"Downloaded conda installer {installer_url} in {t:.1f}s")
|
||||||
|
|
||||||
if sha256_file(f.name) != sha256sum:
|
if sha256sum and sha256_file(f.name) != sha256sum:
|
||||||
raise Exception("sha256sum hash mismatch! Downloaded file corrupted")
|
raise Exception("sha256sum hash mismatch! Downloaded file corrupted")
|
||||||
|
|
||||||
yield f.name
|
yield f.name
|
||||||
@@ -97,39 +105,26 @@ def ensure_conda_packages(prefix, packages):
|
|||||||
Note that conda seem to update dependencies by default, so there is probably
|
Note that conda seem to update dependencies by default, so there is probably
|
||||||
no need to have a update parameter exposed for this function.
|
no need to have a update parameter exposed for this function.
|
||||||
"""
|
"""
|
||||||
conda_executable = [os.path.join(prefix, "bin", "mamba")]
|
conda_executable = os.path.join(prefix, "bin", "mamba")
|
||||||
|
if not os.path.isfile(conda_executable):
|
||||||
|
# fallback on conda if mamba is not present (e.g. for mamba to install itself)
|
||||||
|
conda_executable = os.path.join(prefix, "bin", "conda")
|
||||||
|
|
||||||
abspath = os.path.abspath(prefix)
|
abspath = os.path.abspath(prefix)
|
||||||
# Let subprocess errors propagate
|
|
||||||
# Explicitly do *not* capture stderr, since that's not always JSON!
|
utils.run_subprocess(
|
||||||
# Scripting conda is a PITA!
|
[
|
||||||
# FIXME: raise different exception when using
|
conda_executable,
|
||||||
raw_output = subprocess.check_output(
|
|
||||||
conda_executable
|
|
||||||
+ [
|
|
||||||
"install",
|
"install",
|
||||||
|
"-y",
|
||||||
"-c",
|
"-c",
|
||||||
"conda-forge", # Make customizable if we ever need to
|
"conda-forge", # Make customizable if we ever need to
|
||||||
"--json",
|
|
||||||
"--prefix",
|
"--prefix",
|
||||||
abspath,
|
abspath,
|
||||||
]
|
]
|
||||||
+ packages
|
+ packages,
|
||||||
).decode()
|
input="",
|
||||||
# `conda install` outputs JSON lines for fetch updates,
|
|
||||||
# and a undelimited output at the end. There is no reasonable way to
|
|
||||||
# parse this outside of this kludge.
|
|
||||||
filtered_output = "\n".join(
|
|
||||||
[
|
|
||||||
l
|
|
||||||
for l in raw_output.split("\n")
|
|
||||||
# Sometimes the JSON messages start with a \x00. The lstrip removes these.
|
|
||||||
# conda messages seem to randomly throw \x00 in places for no reason
|
|
||||||
if not l.lstrip("\x00").startswith('{"fetch"')
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
output = json.loads(filtered_output.lstrip("\x00"))
|
|
||||||
if "success" in output and output["success"] == True:
|
|
||||||
return
|
|
||||||
fix_permissions(prefix)
|
fix_permissions(prefix)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from tljh import (
|
|||||||
traefik,
|
traefik,
|
||||||
user,
|
user,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .config import (
|
from .config import (
|
||||||
CONFIG_DIR,
|
CONFIG_DIR,
|
||||||
CONFIG_FILE,
|
CONFIG_FILE,
|
||||||
@@ -34,6 +35,7 @@ from .config import (
|
|||||||
STATE_DIR,
|
STATE_DIR,
|
||||||
USER_ENV_PREFIX,
|
USER_ENV_PREFIX,
|
||||||
)
|
)
|
||||||
|
from .utils import parse_version as V
|
||||||
from .yaml import yaml
|
from .yaml import yaml
|
||||||
|
|
||||||
HERE = os.path.abspath(os.path.dirname(__file__))
|
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||||
@@ -153,60 +155,115 @@ def ensure_usergroups():
|
|||||||
f.write("Defaults exempt_group = jupyterhub-admins\n")
|
f.write("Defaults exempt_group = jupyterhub-admins\n")
|
||||||
|
|
||||||
|
|
||||||
|
# Install mambaforge using an installer from
|
||||||
|
# https://github.com/conda-forge/miniforge/releases
|
||||||
|
MAMBAFORGE_VERSION = "23.1.0-1"
|
||||||
|
# sha256 checksums
|
||||||
|
MAMBAFORGE_CHECKSUMS = {
|
||||||
|
"aarch64": "d9d89c9e349369702171008d9ee7c5ce80ed420e5af60bd150a3db4bf674443a",
|
||||||
|
"x86_64": "cfb16c47dc2d115c8b114280aa605e322173f029fdb847a45348bf4bd23c62ab",
|
||||||
|
}
|
||||||
|
|
||||||
|
# minimum versions of packages
|
||||||
|
MINIMUM_VERSIONS = {
|
||||||
|
# if conda/mamba are lower than this, upgrade them before installing the user packages
|
||||||
|
"mamba": "0.16.0",
|
||||||
|
"conda": "4.10",
|
||||||
|
# minimum Python version (if not matched, abort to avoid big disruptive updates)
|
||||||
|
"python": "3.9",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _mambaforge_url(version=MAMBAFORGE_VERSION, arch=None):
|
||||||
|
"""Return (URL, checksum) for mambaforge download for a given version and arch
|
||||||
|
|
||||||
|
Default values provided for both version and arch
|
||||||
|
"""
|
||||||
|
if arch is None:
|
||||||
|
arch = os.uname().machine
|
||||||
|
installer_url = "https://github.com/conda-forge/miniforge/releases/download/{v}/Mambaforge-{v}-Linux-{arch}.sh".format(
|
||||||
|
v=version,
|
||||||
|
arch=arch,
|
||||||
|
)
|
||||||
|
# Check system architecture, set appropriate installer checksum
|
||||||
|
checksum = MAMBAFORGE_CHECKSUMS.get(arch)
|
||||||
|
if not checksum:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unsupported architecture: {arch}. TLJH only supports {','.join(MAMBAFORGE_CHECKSUMS.keys())}"
|
||||||
|
)
|
||||||
|
return installer_url, checksum
|
||||||
|
|
||||||
|
|
||||||
def ensure_user_environment(user_requirements_txt_file):
|
def ensure_user_environment(user_requirements_txt_file):
|
||||||
"""
|
"""
|
||||||
Set up user conda environment with required packages
|
Set up user conda environment with required packages
|
||||||
"""
|
"""
|
||||||
logger.info("Setting up user environment...")
|
logger.info("Setting up user environment...")
|
||||||
|
|
||||||
miniconda_old_version = "4.5.4"
|
|
||||||
miniconda_new_version = "4.7.10"
|
|
||||||
# Install mambaforge using an installer from
|
|
||||||
# https://github.com/conda-forge/miniforge/releases
|
|
||||||
mambaforge_new_version = "4.10.3-7"
|
|
||||||
# Check system architecture, set appropriate installer checksum
|
|
||||||
if os.uname().machine == "aarch64":
|
|
||||||
installer_sha256 = (
|
|
||||||
"ac95f137b287b3408e4f67f07a284357b1119ee157373b788b34e770ef2392b2"
|
|
||||||
)
|
|
||||||
elif os.uname().machine == "x86_64":
|
|
||||||
installer_sha256 = (
|
|
||||||
"fc872522ec427fcab10167a93e802efaf251024b58cc27b084b915a9a73c4474"
|
|
||||||
)
|
|
||||||
# Check OS, set appropriate string for conda installer path
|
# Check OS, set appropriate string for conda installer path
|
||||||
if os.uname().sysname != "Linux":
|
if os.uname().sysname != "Linux":
|
||||||
raise OSError("TLJH is only supported on Linux platforms.")
|
raise OSError("TLJH is only supported on Linux platforms.")
|
||||||
# Then run `mamba --version` to get the conda and mamba versions
|
|
||||||
# Keep these in sync with tests/test_conda.py::prefix
|
|
||||||
mambaforge_conda_new_version = "4.10.3"
|
|
||||||
mambaforge_mamba_version = "0.16.0"
|
|
||||||
|
|
||||||
if conda.check_miniconda_version(USER_ENV_PREFIX, mambaforge_conda_new_version):
|
# Check the existing environment for what to do
|
||||||
conda_version = "4.10.3"
|
package_versions = conda.get_conda_package_versions(USER_ENV_PREFIX)
|
||||||
elif conda.check_miniconda_version(USER_ENV_PREFIX, miniconda_new_version):
|
|
||||||
conda_version = "4.8.1"
|
# Case 1: no existing environment
|
||||||
elif conda.check_miniconda_version(USER_ENV_PREFIX, miniconda_old_version):
|
if not package_versions:
|
||||||
conda_version = "4.5.8"
|
# 1a. no environment, but prefix exists.
|
||||||
# If no prior miniconda installation is found, we can install a newer version
|
# Abort to avoid clobbering something we don't recognize
|
||||||
else:
|
if os.path.exists(USER_ENV_PREFIX) and os.listdir(USER_ENV_PREFIX):
|
||||||
|
msg = f"Found non-empty directory that is not a conda install in {USER_ENV_PREFIX}. Please remove it (or rename it to preserve files) and run tljh again."
|
||||||
|
logger.error(msg)
|
||||||
|
raise OSError(msg)
|
||||||
|
|
||||||
|
# 1b. No environment, directory empty or doesn't exist
|
||||||
|
# start fresh install
|
||||||
logger.info("Downloading & setting up user environment...")
|
logger.info("Downloading & setting up user environment...")
|
||||||
installer_url = "https://github.com/conda-forge/miniforge/releases/download/{v}/Mambaforge-{v}-Linux-{arch}.sh".format(
|
installer_url, installer_sha256 = _mambaforge_url()
|
||||||
v=mambaforge_new_version, arch=os.uname().machine
|
|
||||||
)
|
|
||||||
with conda.download_miniconda_installer(
|
with conda.download_miniconda_installer(
|
||||||
installer_url, installer_sha256
|
installer_url, installer_sha256
|
||||||
) as installer_path:
|
) as installer_path:
|
||||||
conda.install_miniconda(installer_path, USER_ENV_PREFIX)
|
conda.install_miniconda(installer_path, USER_ENV_PREFIX)
|
||||||
conda_version = "4.10.3"
|
package_versions = conda.get_conda_package_versions(USER_ENV_PREFIX)
|
||||||
|
# quick sanity check: we should have conda and mamba!
|
||||||
|
assert "conda" in package_versions
|
||||||
|
assert "mamba" in package_versions
|
||||||
|
|
||||||
conda.ensure_conda_packages(
|
# next, check Python
|
||||||
USER_ENV_PREFIX,
|
python_version = package_versions["python"]
|
||||||
[
|
logger.debug(f"Found python={python_version} in {USER_ENV_PREFIX}")
|
||||||
# Conda's latest version is on conda much more so than on PyPI.
|
if V(python_version) < V(MINIMUM_VERSIONS["python"]):
|
||||||
"conda==" + conda_version,
|
msg = (
|
||||||
"mamba==" + mambaforge_mamba_version,
|
f"TLJH requires Python >={MINIMUM_VERSIONS['python']}, found python={python_version} in {USER_ENV_PREFIX}."
|
||||||
],
|
f"\nPlease upgrade Python (may be highly disruptive!), or remove/rename {USER_ENV_PREFIX} to allow TLJH to make a fresh install."
|
||||||
)
|
f"\nYou can use `{USER_ENV_PREFIX}/bin/conda list` to save your current list of packages."
|
||||||
|
)
|
||||||
|
logger.error(msg)
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
# at this point, we know we have an env ready with conda and are going to start installing
|
||||||
|
# first, check if we should upgrade/install conda and/or mamba
|
||||||
|
to_upgrade = []
|
||||||
|
for pkg in ("conda", "mamba"):
|
||||||
|
version = package_versions.get(pkg)
|
||||||
|
min_version = MINIMUM_VERSIONS[pkg]
|
||||||
|
if not version:
|
||||||
|
logger.warning(f"{USER_ENV_PREFIX} is missing {pkg}, installing it...")
|
||||||
|
to_upgrade.append(pkg)
|
||||||
|
else:
|
||||||
|
logger.debug(f"Found {pkg}=={version} in {USER_ENV_PREFIX}")
|
||||||
|
if V(version) < V(min_version):
|
||||||
|
logger.info(
|
||||||
|
f"{USER_ENV_PREFIX} has {pkg}=={version}, it will be upgraded to {pkg}>={min_version}"
|
||||||
|
)
|
||||||
|
to_upgrade.append(pkg)
|
||||||
|
|
||||||
|
if to_upgrade:
|
||||||
|
conda.ensure_conda_packages(
|
||||||
|
USER_ENV_PREFIX,
|
||||||
|
# we _could_ explicitly pin Python here,
|
||||||
|
# but conda already does this by default
|
||||||
|
to_upgrade,
|
||||||
|
)
|
||||||
|
|
||||||
conda.ensure_pip_requirements(
|
conda.ensure_pip_requirements(
|
||||||
USER_ENV_PREFIX,
|
USER_ENV_PREFIX,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
Miscellaneous functions useful in at least two places unrelated to each other
|
Miscellaneous functions useful in at least two places unrelated to each other
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
# Copied into bootstrap/bootstrap.py. Make sure these two copies are exactly the same!
|
# Copied into bootstrap/bootstrap.py. Make sure these two copies are exactly the same!
|
||||||
@@ -24,10 +25,11 @@ def run_subprocess(cmd, *args, **kwargs):
|
|||||||
and failed output directly to the user's screen
|
and failed output directly to the user's screen
|
||||||
"""
|
"""
|
||||||
logger = logging.getLogger("tljh")
|
logger = logging.getLogger("tljh")
|
||||||
|
printable_command = " ".join(cmd)
|
||||||
|
logger.debug("Running %s", printable_command)
|
||||||
proc = subprocess.run(
|
proc = subprocess.run(
|
||||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, *args, **kwargs
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, *args, **kwargs
|
||||||
)
|
)
|
||||||
printable_command = " ".join(cmd)
|
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
# Our process failed! Show output to the user
|
# Our process failed! Show output to the user
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -59,3 +61,14 @@ def get_plugin_manager():
|
|||||||
pm.load_setuptools_entrypoints("tljh")
|
pm.load_setuptools_entrypoints("tljh")
|
||||||
|
|
||||||
return pm
|
return pm
|
||||||
|
|
||||||
|
|
||||||
|
def parse_version(version_string):
|
||||||
|
"""Parse version string to tuple
|
||||||
|
|
||||||
|
Finds all numbers and returns a tuple of ints
|
||||||
|
_very_ loose version parsing, like the old distutils.version.LooseVersion
|
||||||
|
"""
|
||||||
|
# return a tuple of all the numbers in the version string
|
||||||
|
# always succeeds, even if passed nonsense
|
||||||
|
return tuple(int(part) for part in re.findall(r"\d+", version_string))
|
||||||
|
|||||||
Reference in New Issue
Block a user