diff --git a/.github/workflows/variants.yml b/.github/workflows/variants.yml index 88106a8..62b7775 100644 --- a/.github/workflows/variants.yml +++ b/.github/workflows/variants.yml @@ -30,14 +30,14 @@ jobs: run: | make VENV_ENABLED=true VENV_CREATE=true PRIMARY_PYTHON=python3 PYTHON_PACKAGE_INSTALLER=pip test make clean - - name: VENV to be created with uv to be installed + - name: VENV to be created with uv (local install, no global UV) run: | - make VENV_ENABLED=true VENV_CREATE=true PYTHON_PACKAGE_INSTALLER=uv PRIMARY_PYTHON=python3 MXENV_UV_GLOBAL=false test + make VENV_ENABLED=true VENV_CREATE=true PYTHON_PACKAGE_INSTALLER=uv PRIMARY_PYTHON=python3 test make clean - - name: VENV to be created with uv globally pre-installed + - name: VENV to be created with uv (global UV auto-detected) run: | pip install uv - make VENV_ENABLED=true VENV_CREATE=true PYTHON_PACKAGE_INSTALLER=uv PRIMARY_PYTHON=${{ matrix.python-version }} MXENV_UV_GLOBAL=true test + make VENV_ENABLED=true VENV_CREATE=true PYTHON_PACKAGE_INSTALLER=uv PRIMARY_PYTHON=${{ matrix.python-version }} test make clean pip uninstall -y uv - name: VENV pre-installed with pip @@ -46,20 +46,26 @@ jobs: make VENV_ENABLED=true VENV_CREATE=false VENV_FOLDER=existingvenv PRIMARY_PYTHON=python3 PYTHON_PACKAGE_INSTALLER=pip test make clean rm -r existingvenv - - name: VENV pre-installed with uv to be installed + - name: VENV pre-installed with uv (local install, no global UV) run: | python -m venv existingvenv - make VENV_ENABLED=true VENV_CREATE=false VENV_FOLDER=existingvenv PRIMARY_PYTHON=python3 PYTHON_PACKAGE_INSTALLER=uv MXENV_UV_GLOBAL=false test + make VENV_ENABLED=true VENV_CREATE=false VENV_FOLDER=existingvenv PRIMARY_PYTHON=python3 PYTHON_PACKAGE_INSTALLER=uv test make clean rm -r existingvenv - - name: VENV pre-installed with uv globally pre-installed + - name: VENV pre-installed with uv (global UV auto-detected) run: | python -m venv existingvenv pip install uv - make VENV_ENABLED=true VENV_CREATE=false VENV_FOLDER=existingvenv PYTHON_PACKAGE_INSTALLER=uv PRIMARY_PYTHON=python3 MXENV_UV_GLOBAL=true test + make VENV_ENABLED=true VENV_CREATE=false VENV_FOLDER=existingvenv PYTHON_PACKAGE_INSTALLER=uv PRIMARY_PYTHON=python3 test make clean pip uninstall -y uv rm -r existingvenv + - name: VENV with global UV using different UV_PYTHON + run: | + pip install uv + make VENV_ENABLED=true VENV_CREATE=true PYTHON_PACKAGE_INSTALLER=uv PRIMARY_PYTHON=python3 UV_PYTHON=${{ matrix.python-version }} test + make clean + pip uninstall -y uv - name: Global Python with pip run: | make VENV_ENABLED=false VENV_CREATE=false PRIMARY_PYTHON=python3 PYTHON_PACKAGE_INSTALLER=pip test diff --git a/CHANGES.md b/CHANGES.md index b83032d..4800c24 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,25 @@ - Fix: interactive uv venv, use --allow-existing instead. - Feature: Add support for Python 3.14. - Feature: Add `docs-linkcheck` to check for broken links in the sphinx documentation. +- Breaking: Removed `MXENV_UV_GLOBAL` setting in favor of automatic UV detection. + When `PYTHON_PACKAGE_INSTALLER=uv`, mxmake now automatically detects and uses + a globally installed `uv` if available. To force local installation of uv, + simply don't install it globally or remove it from PATH. +- Feature: Add `UV_PYTHON` setting to specify Python version for UV-managed virtual + environments. Defaults to `PRIMARY_PYTHON` for backward compatibility. This + provides semantic clarity: `PRIMARY_PYTHON` is the system interpreter path + (e.g., `python3.11`), while `UV_PYTHON` is the version spec for UV + (e.g., `3.14`, `cpython@3.14`). +- Feature: Automatic detection of global UV installation using simple shell check. + No manual configuration required. +- Feature: All UV commands now run with `--quiet --no-progress` flags for better + CI/CD compatibility and cleaner log output. +- Feature: When using global UV, mxmake checks if updates are available using + `uv self update --dry-run` and displays a helpful non-blocking warning if a + newer version is available. +- Improvement: Simplified mxenv.mk logic from 3+ nesting levels to 1-2 levels + using computed intermediate variables (`USE_GLOBAL_UV`, `USE_LOCAL_UV`). + Code is now more maintainable and easier to extend. ## 1.3.0 (2025-09-03) diff --git a/Makefile b/Makefile index 3c6c9f9..38cb2cc 100644 --- a/Makefile +++ b/Makefile @@ -57,18 +57,19 @@ PRIMARY_PYTHON?=3.14 PYTHON_MIN_VERSION?=3.9 # Install packages using the given package installer method. -# Supported are `pip` and `uv`. If uv is used, its global availability is -# checked. Otherwise, it is installed, either in the virtual environment or -# using the `PRIMARY_PYTHON`, dependent on the `VENV_ENABLED` setting. If -# `VENV_ENABLED` and uv is selected, uv is used to create the virtual -# environment. +# Supported are `pip` and `uv`. When `uv` is selected, a global installation +# is auto-detected and used if available. Otherwise, uv is installed in the +# virtual environment or using `PRIMARY_PYTHON`, depending on the +# `VENV_ENABLED` setting. # Default: pip PYTHON_PACKAGE_INSTALLER?=uv -# Flag whether to use a global installed 'uv' or install -# it in the virtual environment. -# Default: false -MXENV_UV_GLOBAL?=true +# Python version for UV to install/use when creating virtual +# environments with global UV. Passed to `uv venv -p VALUE`. Supports version +# specs like `3.11`, `3.14`, `cpython@3.14`. Defaults to PRIMARY_PYTHON value +# for backward compatibility. +# Default: $(PRIMARY_PYTHON) +UV_PYTHON?=$(PRIMARY_PYTHON) # Flag whether to use virtual environment. If `false`, the # interpreter according to `PRIMARY_PYTHON` found in `PATH` is used. @@ -233,30 +234,57 @@ else MXENV_PYTHON=$(PRIMARY_PYTHON) endif -# Determine the package installer +# Determine the package installer with non-interactive flags ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") -PYTHON_PACKAGE_COMMAND=uv pip +PYTHON_PACKAGE_COMMAND=uv pip --quiet --no-progress else PYTHON_PACKAGE_COMMAND=$(MXENV_PYTHON) -m pip endif +# Auto-detect global uv availability (simple existence check) +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +UV_AVAILABLE:=$(shell command -v uv >/dev/null 2>&1 && echo "true" || echo "false") +else +UV_AVAILABLE:=false +endif + +# Determine installation strategy +USE_GLOBAL_UV:=$(shell [[ "$(PYTHON_PACKAGE_INSTALLER)" == "uv" && "$(UV_AVAILABLE)" == "true" ]] && echo "true" || echo "false") +USE_LOCAL_UV:=$(shell [[ "$(PYTHON_PACKAGE_INSTALLER)" == "uv" && "$(UV_AVAILABLE)" == "false" ]] && echo "true" || echo "false") + +# Check if global UV is outdated (non-blocking warning) +ifeq ("$(USE_GLOBAL_UV)","true") +UV_OUTDATED:=$(shell uv self update --dry-run 2>&1 | grep -q "Would update" && echo "true" || echo "false") +else +UV_OUTDATED:=false +endif + MXENV_TARGET:=$(SENTINEL_FOLDER)/mxenv.sentinel $(MXENV_TARGET): $(SENTINEL) -ifneq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse") + # Validation: Check Python version if not using global uv +ifneq ("$(USE_GLOBAL_UV)","true") @$(PRIMARY_PYTHON) -c "import sys; vi = sys.version_info; sys.exit(1 if (int(vi[0]), int(vi[1])) >= tuple(map(int, '$(PYTHON_MIN_VERSION)'.split('.'))) else 0)" \ && echo "Need Python >= $(PYTHON_MIN_VERSION)" && exit 1 || : else - @echo "Use Python $(PYTHON_MIN_VERSION) over uv" + @echo "Using global uv for Python $(UV_PYTHON)" endif + # Validation: Check VENV_FOLDER is set if venv enabled @[[ "$(VENV_ENABLED)" == "true" && "$(VENV_FOLDER)" == "" ]] \ && echo "VENV_FOLDER must be configured if VENV_ENABLED is true" && exit 1 || : - @[[ "$(VENV_ENABLED)$(PYTHON_PACKAGE_INSTALLER)" == "falseuv" ]] \ + # Validation: Check uv not used with system Python + @[[ "$(VENV_ENABLED)" == "false" && "$(PYTHON_PACKAGE_INSTALLER)" == "uv" ]] \ && echo "Package installer uv does not work with a global Python interpreter." && exit 1 || : + # Warning: Notify if global UV is outdated +ifeq ("$(UV_OUTDATED)","true") + @echo "WARNING: A newer version of uv is available. Run 'uv self update' to upgrade." +endif + + # Create virtual environment ifeq ("$(VENV_ENABLED)", "true") ifeq ("$(VENV_CREATE)", "true") -ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvtrue") - @echo "Setup Python Virtual Environment using package 'uv' at '$(VENV_FOLDER)'" - @uv venv -p $(PRIMARY_PYTHON) --seed $(VENV_FOLDER) +ifeq ("$(USE_GLOBAL_UV)","true") + @echo "Setup Python Virtual Environment using global uv at '$(VENV_FOLDER)'" + @uv venv --quiet --no-progress -p $(UV_PYTHON) --seed $(VENV_FOLDER) else @echo "Setup Python Virtual Environment using module 'venv' at '$(VENV_FOLDER)'" @$(PRIMARY_PYTHON) -m venv $(VENV_FOLDER) @@ -266,10 +294,14 @@ endif else @echo "Using system Python interpreter" endif -ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse") - @echo "Install uv" + + # Install uv locally if needed +ifeq ("$(USE_LOCAL_UV)","true") + @echo "Install uv in virtual environment" @$(MXENV_PYTHON) -m pip install uv endif + + # Install/upgrade core packages @$(PYTHON_PACKAGE_COMMAND) install -U pip setuptools wheel @echo "Install/Update MXStack Python packages" @$(PYTHON_PACKAGE_COMMAND) install -U $(MXDEV) $(MXMAKE) diff --git a/docs/source/getting-started.md b/docs/source/getting-started.md index 41b8ecd..ac65fcf 100644 --- a/docs/source/getting-started.md +++ b/docs/source/getting-started.md @@ -63,6 +63,23 @@ For details read the chapter [on topics and it's domains](topics-and-domains). Do not add custom settings to settings section. They will be lost on next `mxmake init` respective `mxmake update` run. +### Python Package Installer + +By default, mxmake uses `pip` as the package installer. You can switch to [UV](https://docs.astral.sh/uv/) by setting `PYTHON_PACKAGE_INSTALLER=uv` in the settings section. + +When using UV, mxmake automatically detects if UV is installed globally or installs it locally in the virtual environment. + +```{note} +When using UV, you should explicitly set `UV_PYTHON` to specify which Python version UV should use. While `UV_PYTHON` currently defaults to `PRIMARY_PYTHON` for backward compatibility, this default may change in future versions. Set `UV_PYTHON` explicitly to avoid surprises. +``` + +Example: +```makefile +PRIMARY_PYTHON?=python3 +PYTHON_PACKAGE_INSTALLER?=uv +UV_PYTHON?=3.14 +``` + ## How to use on the Windows operating system mxmake works excellent on Windows! diff --git a/docs/source/preseeds.md b/docs/source/preseeds.md index 4236335..87c84d1 100644 --- a/docs/source/preseeds.md +++ b/docs/source/preseeds.md @@ -15,9 +15,10 @@ topics: core: # include domain mxenv mxenv: - # set PYTHON_MIN_VERSION and PYTHON_PACKAGE_INSTALLER - PYTHON_MIN_VERSION: 3.10 + # set PYTHON_MIN_VERSION, PYTHON_PACKAGE_INSTALLER and UV_PYTHON + PYTHON_MIN_VERSION: 3.14 PYTHON_PACKAGE_INSTALLER: uv + UV_PYTHON: "3.14" qa: # include domains from qa topic but do not override default settings ruff: @@ -36,6 +37,26 @@ Now initialize the project with the preseeds: $ mxmake init -p preseeds.yaml ``` +## UV Package Installer + +When `PYTHON_PACKAGE_INSTALLER` is set to `uv`, mxmake automatically detects whether UV is installed globally on your system. + +```{important} +When using UV, you should explicitly set `UV_PYTHON` to specify which Python version UV should use. While `UV_PYTHON` currently defaults to `PRIMARY_PYTHON` for backward compatibility, relying on this default is not recommended and may change in future versions. +``` + +The `UV_PYTHON` setting accepts version specs like `3.13`, `3.14`, or `cpython@3.14`: + +```yaml +topics: + core: + mxenv: + PYTHON_MIN_VERSION: "3.14" + PRIMARY_PYTHON: python3 + PYTHON_PACKAGE_INSTALLER: uv + UV_PYTHON: "3.14" # Explicitly specify Python version for UV +``` + ## Examples ### Create a simple Python project @@ -56,9 +77,9 @@ Enter the `hello-world-` directory and create a file `preseed.yaml`: topics: core: mxenv: - PYTHON_MIN_VERSION: "3.10" + PYTHON_MIN_VERSION: "3.14" PYTHON_PACKAGE_INSTALLER: uv - MXENV_UV_GLOBAL: true + UV_PYTHON: "3.14" sources: qa: ruff @@ -103,9 +124,9 @@ topics: base: RUN_TARGET: zope-start mxenv: - PYTHON_MIN_VERSION: "3.10" + PYTHON_MIN_VERSION: "3.13" PYTHON_PACKAGE_INSTALLER: uv - MXENV_UV_GLOBAL: true + UV_PYTHON: "3.13" applications: zope: plone: diff --git a/src/mxmake/tests/expected/Makefile b/src/mxmake/tests/expected/Makefile index cddc773..795c63b 100644 --- a/src/mxmake/tests/expected/Makefile +++ b/src/mxmake/tests/expected/Makefile @@ -48,18 +48,19 @@ PRIMARY_PYTHON?=python3 PYTHON_MIN_VERSION?=3.9 # Install packages using the given package installer method. -# Supported are `pip` and `uv`. If uv is used, its global availability is -# checked. Otherwise, it is installed, either in the virtual environment or -# using the `PRIMARY_PYTHON`, dependent on the `VENV_ENABLED` setting. If -# `VENV_ENABLED` and uv is selected, uv is used to create the virtual -# environment. +# Supported are `pip` and `uv`. When `uv` is selected, a global installation +# is auto-detected and used if available. Otherwise, uv is installed in the +# virtual environment or using `PRIMARY_PYTHON`, depending on the +# `VENV_ENABLED` setting. # Default: pip PYTHON_PACKAGE_INSTALLER?=pip -# Flag whether to use a global installed 'uv' or install -# it in the virtual environment. -# Default: false -MXENV_UV_GLOBAL?=false +# Python version for UV to install/use when creating virtual +# environments with global UV. Passed to `uv venv -p VALUE`. Supports version +# specs like `3.11`, `3.14`, `cpython@3.14`. Defaults to PRIMARY_PYTHON value +# for backward compatibility. +# Default: $(PRIMARY_PYTHON) +UV_PYTHON?=$(PRIMARY_PYTHON) # Flag whether to use virtual environment. If `false`, the # interpreter according to `PRIMARY_PYTHON` found in `PATH` is used. @@ -139,30 +140,61 @@ else MXENV_PYTHON=$(PRIMARY_PYTHON) endif -# Determine the package installer +# Determine the package installer with non-interactive flags ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") -PYTHON_PACKAGE_COMMAND=uv pip +PYTHON_PACKAGE_COMMAND=uv pip --no-progress else PYTHON_PACKAGE_COMMAND=$(MXENV_PYTHON) -m pip endif +# Auto-detect global uv availability (simple existence check) +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +UV_AVAILABLE:=$(shell command -v uv >/dev/null 2>&1 && echo "true" || echo "false") +else +UV_AVAILABLE:=false +endif + +# Determine installation strategy +# depending on the PYTHON_PACKAGE_INSTALLER and UV_AVAILABLE +# - both vars can be false or +# - one of them can be true, +# - but never boths. +USE_GLOBAL_UV:=$(shell [[ "$(PYTHON_PACKAGE_INSTALLER)" == "uv" && "$(UV_AVAILABLE)" == "true" ]] && echo "true" || echo "false") +USE_LOCAL_UV:=$(shell [[ "$(PYTHON_PACKAGE_INSTALLER)" == "uv" && "$(UV_AVAILABLE)" == "false" ]] && echo "true" || echo "false") + +# Check if global UV is outdated (non-blocking warning) +ifeq ("$(USE_GLOBAL_UV)","true") +UV_OUTDATED:=$(shell uv self update --dry-run 2>&1 | grep -q "Would update" && echo "true" || echo "false") +else +UV_OUTDATED:=false +endif + MXENV_TARGET:=$(SENTINEL_FOLDER)/mxenv.sentinel $(MXENV_TARGET): $(SENTINEL) -ifneq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse") + # Validation: Check Python version if not using global uv +ifneq ("$(USE_GLOBAL_UV)","true") @$(PRIMARY_PYTHON) -c "import sys; vi = sys.version_info; sys.exit(1 if (int(vi[0]), int(vi[1])) >= tuple(map(int, '$(PYTHON_MIN_VERSION)'.split('.'))) else 0)" \ && echo "Need Python >= $(PYTHON_MIN_VERSION)" && exit 1 || : else - @echo "Use Python $(PYTHON_MIN_VERSION) over uv" + @echo "Using global uv for Python $(UV_PYTHON)" endif + # Validation: Check VENV_FOLDER is set if venv enabled @[[ "$(VENV_ENABLED)" == "true" && "$(VENV_FOLDER)" == "" ]] \ && echo "VENV_FOLDER must be configured if VENV_ENABLED is true" && exit 1 || : - @[[ "$(VENV_ENABLED)$(PYTHON_PACKAGE_INSTALLER)" == "falseuv" ]] \ + # Validation: Check uv not used with system Python + @[[ "$(VENV_ENABLED)" == "false" && "$(PYTHON_PACKAGE_INSTALLER)" == "uv" ]] \ && echo "Package installer uv does not work with a global Python interpreter." && exit 1 || : + # Warning: Notify if global UV is outdated +ifeq ("$(UV_OUTDATED)","true") + @echo "WARNING: A newer version of uv is available. Run 'uv self update' to upgrade." +endif + + # Create virtual environment ifeq ("$(VENV_ENABLED)", "true") ifeq ("$(VENV_CREATE)", "true") -ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvtrue") - @echo "Setup Python Virtual Environment using package 'uv' at '$(VENV_FOLDER)'" - @uv venv -p $(PRIMARY_PYTHON) --seed --allow-existing $(VENV_FOLDER) +ifeq ("$(USE_GLOBAL_UV)","true") + @echo "Setup Python Virtual Environment using global uv at '$(VENV_FOLDER)'" + @uv venv --allow-existing --no-progress -p $(UV_PYTHON) --seed $(VENV_FOLDER) else @echo "Setup Python Virtual Environment using module 'venv' at '$(VENV_FOLDER)'" @$(PRIMARY_PYTHON) -m venv $(VENV_FOLDER) @@ -172,10 +204,14 @@ endif else @echo "Using system Python interpreter" endif -ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse") - @echo "Install uv" + + # Install uv locally if needed +ifeq ("$(USE_LOCAL_UV)","true") + @echo "Install uv in virtual environment" @$(MXENV_PYTHON) -m pip install uv endif + + # Install/upgrade core packages @$(PYTHON_PACKAGE_COMMAND) install -U pip setuptools wheel @echo "Install/Update MXStack Python packages" @$(PYTHON_PACKAGE_COMMAND) install -U $(MXDEV) $(MXMAKE) diff --git a/src/mxmake/tests/test_parser.py b/src/mxmake/tests/test_parser.py index 50b7d6d..98aa721 100644 --- a/src/mxmake/tests/test_parser.py +++ b/src/mxmake/tests/test_parser.py @@ -21,7 +21,7 @@ def test_MakefileParser(self, tempdir): "core.mxenv.PRIMARY_PYTHON": "python3", "core.mxenv.PYTHON_MIN_VERSION": "3.7", "core.mxenv.PYTHON_PACKAGE_INSTALLER": "pip", - "core.mxenv.MXENV_UV_GLOBAL": "false", + "core.mxenv.UV_PYTHON": "$(PRIMARY_PYTHON)", "core.mxenv.VENV_ENABLED": "true", "core.mxenv.VENV_CREATE": "true", "core.mxenv.VENV_FOLDER": "venv", @@ -67,7 +67,7 @@ def test_MakefileParser(self, tempdir): "core.mxenv.PRIMARY_PYTHON": "python3", "core.mxenv.PYTHON_MIN_VERSION": "3.7", "core.mxenv.PYTHON_PACKAGE_INSTALLER": "pip", - "core.mxenv.MXENV_UV_GLOBAL": "false", + "core.mxenv.UV_PYTHON": "$(PRIMARY_PYTHON)", "core.mxenv.VENV_ENABLED": "true", "core.mxenv.VENV_CREATE": "true", "core.mxenv.VENV_FOLDER": "venv", diff --git a/src/mxmake/tests/test_templates.py b/src/mxmake/tests/test_templates.py index e26f734..1c7c6e7 100644 --- a/src/mxmake/tests/test_templates.py +++ b/src/mxmake/tests/test_templates.py @@ -564,7 +564,7 @@ def test_Makefile(self, tempdir): "core.mxenv.PRIMARY_PYTHON": "python3", "core.mxenv.PYTHON_MIN_VERSION": "3.9", "core.mxenv.PYTHON_PACKAGE_INSTALLER": "pip", - "core.mxenv.MXENV_UV_GLOBAL": "false", + "core.mxenv.UV_PYTHON": "$(PRIMARY_PYTHON)", "core.mxenv.VENV_ENABLED": "true", "core.mxenv.VENV_CREATE": "true", "core.mxenv.VENV_FOLDER": ".venv", diff --git a/src/mxmake/topics/core/mxenv.mk b/src/mxmake/topics/core/mxenv.mk index d83c8c4..d479ce4 100644 --- a/src/mxmake/topics/core/mxenv.mk +++ b/src/mxmake/topics/core/mxenv.mk @@ -35,17 +35,18 @@ #: #:[setting.PYTHON_PACKAGE_INSTALLER] #:description = Install packages using the given package installer method. -#: Supported are `pip` and `uv`. If uv is used, its global availability is -#: checked. Otherwise, it is installed, either in the virtual environment or -#: using the `PRIMARY_PYTHON`, dependent on the `VENV_ENABLED` setting. If -#: `VENV_ENABLED` and uv is selected, uv is used to create the virtual -#: environment. +#: Supported are `pip` and `uv`. When `uv` is selected, a global installation +#: is auto-detected and used if available. Otherwise, uv is installed in the +#: virtual environment or using `PRIMARY_PYTHON`, depending on the +#: `VENV_ENABLED` setting. #:default = pip #: -#:[setting.MXENV_UV_GLOBAL] -#:description = Flag whether to use a global installed 'uv' or install -#: it in the virtual environment. -#:default = false +#:[setting.UV_PYTHON] +#:description = Python version for UV to install/use when creating virtual +#: environments with global UV. Passed to `uv venv -p VALUE`. Supports version +#: specs like `3.11`, `3.14`, `cpython@3.14`. Defaults to PRIMARY_PYTHON value +#: for backward compatibility. +#:default = $(PRIMARY_PYTHON) #: #:[setting.VENV_ENABLED] #:description = Flag whether to use virtual environment. If `false`, the @@ -94,30 +95,61 @@ else MXENV_PYTHON=$(PRIMARY_PYTHON) endif -# Determine the package installer +# Determine the package installer with non-interactive flags ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") -PYTHON_PACKAGE_COMMAND=uv pip +PYTHON_PACKAGE_COMMAND=uv pip --no-progress else PYTHON_PACKAGE_COMMAND=$(MXENV_PYTHON) -m pip endif +# Auto-detect global uv availability (simple existence check) +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +UV_AVAILABLE:=$(shell command -v uv >/dev/null 2>&1 && echo "true" || echo "false") +else +UV_AVAILABLE:=false +endif + +# Determine installation strategy +# depending on the PYTHON_PACKAGE_INSTALLER and UV_AVAILABLE +# - both vars can be false or +# - one of them can be true, +# - but never boths. +USE_GLOBAL_UV:=$(shell [[ "$(PYTHON_PACKAGE_INSTALLER)" == "uv" && "$(UV_AVAILABLE)" == "true" ]] && echo "true" || echo "false") +USE_LOCAL_UV:=$(shell [[ "$(PYTHON_PACKAGE_INSTALLER)" == "uv" && "$(UV_AVAILABLE)" == "false" ]] && echo "true" || echo "false") + +# Check if global UV is outdated (non-blocking warning) +ifeq ("$(USE_GLOBAL_UV)","true") +UV_OUTDATED:=$(shell uv self update --dry-run 2>&1 | grep -q "Would update" && echo "true" || echo "false") +else +UV_OUTDATED:=false +endif + MXENV_TARGET:=$(SENTINEL_FOLDER)/mxenv.sentinel $(MXENV_TARGET): $(SENTINEL) -ifneq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse") + # Validation: Check Python version if not using global uv +ifneq ("$(USE_GLOBAL_UV)","true") @$(PRIMARY_PYTHON) -c "import sys; vi = sys.version_info; sys.exit(1 if (int(vi[0]), int(vi[1])) >= tuple(map(int, '$(PYTHON_MIN_VERSION)'.split('.'))) else 0)" \ && echo "Need Python >= $(PYTHON_MIN_VERSION)" && exit 1 || : else - @echo "Use Python $(PYTHON_MIN_VERSION) over uv" + @echo "Using global uv for Python $(UV_PYTHON)" endif + # Validation: Check VENV_FOLDER is set if venv enabled @[[ "$(VENV_ENABLED)" == "true" && "$(VENV_FOLDER)" == "" ]] \ && echo "VENV_FOLDER must be configured if VENV_ENABLED is true" && exit 1 || : - @[[ "$(VENV_ENABLED)$(PYTHON_PACKAGE_INSTALLER)" == "falseuv" ]] \ + # Validation: Check uv not used with system Python + @[[ "$(VENV_ENABLED)" == "false" && "$(PYTHON_PACKAGE_INSTALLER)" == "uv" ]] \ && echo "Package installer uv does not work with a global Python interpreter." && exit 1 || : + # Warning: Notify if global UV is outdated +ifeq ("$(UV_OUTDATED)","true") + @echo "WARNING: A newer version of uv is available. Run 'uv self update' to upgrade." +endif + + # Create virtual environment ifeq ("$(VENV_ENABLED)", "true") ifeq ("$(VENV_CREATE)", "true") -ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvtrue") - @echo "Setup Python Virtual Environment using package 'uv' at '$(VENV_FOLDER)'" - @uv venv -p $(PRIMARY_PYTHON) --seed --allow-existing $(VENV_FOLDER) +ifeq ("$(USE_GLOBAL_UV)","true") + @echo "Setup Python Virtual Environment using global uv at '$(VENV_FOLDER)'" + @uv venv --allow-existing --no-progress -p $(UV_PYTHON) --seed $(VENV_FOLDER) else @echo "Setup Python Virtual Environment using module 'venv' at '$(VENV_FOLDER)'" @$(PRIMARY_PYTHON) -m venv $(VENV_FOLDER) @@ -127,10 +159,14 @@ endif else @echo "Using system Python interpreter" endif -ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse") - @echo "Install uv" + + # Install uv locally if needed +ifeq ("$(USE_LOCAL_UV)","true") + @echo "Install uv in virtual environment" @$(MXENV_PYTHON) -m pip install uv endif + + # Install/upgrade core packages @$(PYTHON_PACKAGE_COMMAND) install -U pip setuptools wheel @echo "Install/Update MXStack Python packages" @$(PYTHON_PACKAGE_COMMAND) install -U $(MXDEV) $(MXMAKE)