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
22 changes: 14 additions & 8 deletions .github/workflows/variants.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
70 changes: 51 additions & 19 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions docs/source/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
33 changes: 27 additions & 6 deletions docs/source/preseeds.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
74 changes: 55 additions & 19 deletions src/mxmake/tests/expected/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Loading