diff --git a/.github/workflows/unit-test.yaml b/.github/workflows/unit-test.yaml index 4dd1e4ec7..96b22bdfb 100644 --- a/.github/workflows/unit-test.yaml +++ b/.github/workflows/unit-test.yaml @@ -81,10 +81,7 @@ jobs: ${{ hashFiles('setup.py', 'dev-requirements.txt', '.github/workflows/unit-test.yaml') }} - name: Install Python dependencies - # Keep pip version pinning in sync with the one in bootstrap.py! - # See changelog at https://pip.pypa.io/en/latest/news/#changelog run: | - python3 -m pip install -U "pip==23.1.*" python3 -m pip install -r dev-requirements.txt python3 -m pip install -e . pip freeze diff --git a/bootstrap/bootstrap.py b/bootstrap/bootstrap.py index 9205f8fea..50b09b49c 100644 --- a/bootstrap/bootstrap.py +++ b/bootstrap/bootstrap.py @@ -476,10 +476,8 @@ def serve_forever(server): run_subprocess(["python3", "-m", "venv", hub_env_prefix]) # Upgrade pip - # Keep pip version pinning in sync with the one in unit-test.yml! - # See changelog at https://pip.pypa.io/en/latest/news/#changelog - logger.info("Upgrading pip...") - run_subprocess([hub_env_pip, "install", "--upgrade", "pip==23.1.*"]) + logger.info("Ensuring pip>=23.1.2...") + run_subprocess([hub_env_pip, "install", "pip>=23.1.2"]) # Install/upgrade TLJH installer tljh_install_cmd = [hub_env_pip, "install", "--upgrade"] diff --git a/tljh/conda.py b/tljh/conda.py index 9f8de5f40..f616abb29 100644 --- a/tljh/conda.py +++ b/tljh/conda.py @@ -28,14 +28,17 @@ def sha256_file(fname): return hash_sha256.hexdigest() -def get_conda_package_versions(prefix): - """Get conda package versions, via `conda list --json`""" +def get_conda_package_versions(prefix, regex_filter=None): + """ + Get conda package versions for the environment in `prefix`, optionally + filtered by a regex. + """ versions = {} + cmd = [os.path.join(prefix, "bin", "conda"), "list", "--json"] + if regex_filter: + cmd.append(regex_filter) try: - out = subprocess.check_output( - [os.path.join(prefix, "bin", "conda"), "list", "--json"], - text=True, - ) + out = subprocess.check_output(cmd, text=True) except (subprocess.CalledProcessError, FileNotFoundError): return versions diff --git a/tljh/installer.py b/tljh/installer.py index 0054ab9ad..926ffbfd3 100644 --- a/tljh/installer.py +++ b/tljh/installer.py @@ -158,9 +158,10 @@ def ensure_usergroups(): # minimum versions of packages MINIMUM_VERSIONS = { - # if conda/mamba are lower than this, upgrade them before installing the user packages + # if conda/mamba/pip are lower than this, upgrade them before installing the user packages "mamba": "0.16.0", "conda": "4.10", + "pip": "23.1.2", # minimum Python version (if not matched, abort to avoid big disruptive updates) "python": "3.9", } @@ -197,20 +198,16 @@ def ensure_user_environment(user_requirements_txt_file): # Check the existing environment for what to do package_versions = conda.get_conda_package_versions(USER_ENV_PREFIX) + is_fresh_install = not package_versions - # Case 1: no existing environment - if not package_versions: - is_fresh_install = True - - # 1a. no environment, but prefix exists. - # Abort to avoid clobbering something we don't recognize + if is_fresh_install: + # If no Python environment is detected but a folder exists we abort to + # avoid clobbering something we don't recognize. 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...") installer_url, installer_sha256 = _mambaforge_url() with conda.download_miniconda_installer( @@ -218,13 +215,12 @@ def ensure_user_environment(user_requirements_txt_file): ) as installer_path: conda.install_miniconda(installer_path, USER_ENV_PREFIX) 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 - else: - is_fresh_install = False - # next, check Python + # Check Python version python_version = package_versions["python"] logger.debug(f"Found python={python_version} in {USER_ENV_PREFIX}") if V(python_version) < V(MINIMUM_VERSIONS["python"]): @@ -236,10 +232,13 @@ def ensure_user_environment(user_requirements_txt_file): 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 + # Ensure minimum versions of: + # + # - conda/mamba, via conda-forge + # - pip, via PyPI + # to_upgrade = [] - for pkg in ("conda", "mamba"): + for pkg in ("conda", "mamba", "pip"): version = package_versions.get(pkg) min_version = MINIMUM_VERSIONS[pkg] if not version: @@ -253,24 +252,33 @@ def ensure_user_environment(user_requirements_txt_file): ) 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, - ) + cf_pkgs_to_upgrade = list(set(to_upgrade) & {"conda", "mamba"}) + if cf_pkgs_to_upgrade: + conda.ensure_conda_packages( + USER_ENV_PREFIX, + # we _could_ explicitly pin Python here, + # but conda already does this by default + cf_pkgs_to_upgrade, + ) + pypi_pkgs_to_upgrade = list(set(to_upgrade) & {"pip"}) + if pypi_pkgs_to_upgrade: + conda.ensure_pip_packages( + USER_ENV_PREFIX, + pypi_pkgs_to_upgrade, + upgrade=True, + ) - conda.ensure_pip_requirements( - USER_ENV_PREFIX, - os.path.join(HERE, "requirements-user-env.txt"), - upgrade=True, - ) + # Install a matching jupyterhub version in the hub env + # + jupyterhub_version = conda.get_conda_package_versions(HUB_ENV_PREFIX, "^jupyterhub$").get("jupyterhub") + conda.ensure_pip_packages(USER_ENV_PREFIX, [f"jupyterhub=={jupyterhub_version}"]) + + # Install user environment extras for initial installations + # if is_fresh_install: conda.ensure_pip_requirements( USER_ENV_PREFIX, os.path.join(HERE, "requirements-user-env-extras.txt"), - upgrade=True, ) if user_requirements_txt_file: diff --git a/tljh/requirements-user-env.txt b/tljh/requirements-user-env.txt deleted file mode 100644 index a9528e90f..000000000 --- a/tljh/requirements-user-env.txt +++ /dev/null @@ -1,7 +0,0 @@ -# When tljh.installer runs, the users' environment as typically found in -# /opt/tljh/user, is installed or upgraded with these packages. -# -# Whats listed here should be only the unconditional requirements that we have in -# the user environment. -# -jupyterhub==4.*