Skip to content
Draft
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
77 changes: 38 additions & 39 deletions .github/workflows/variants.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,70 +6,69 @@ on:
workflow_dispatch:

jobs:
build:
pip-variant:
strategy:
fail-fast: false
matrix:
python-version:
# we test on lowest and highest supported versions
- "3.10"
- "3.14"
os:
- ubuntu-latest
- macos-latest
- windows-latest
# lowest and highest supported versions
python-version: ["3.10", "3.14"]
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
- name: VENV to be created with pip
- name: Switch project Makefile to the pip variant
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 (local install, no global UV)
run: |
make VENV_ENABLED=true VENV_CREATE=true PYTHON_PACKAGE_INSTALLER=uv PRIMARY_PYTHON=python3 test
make clean
- name: VENV to be created with uv (global UV auto-detected)
python -m pip install -e .
sed -i.bak 's/^#: core.mxenv-uv/#: core.mxenv-pip/' Makefile && rm -f Makefile.bak
mxmake update
- name: VENV created with pip
run: |
pip install uv
make VENV_ENABLED=true VENV_CREATE=true PYTHON_PACKAGE_INSTALLER=uv PRIMARY_PYTHON=${{ matrix.python-version }} test
make VENV_ENABLED=true VENV_CREATE=true PRIMARY_PYTHON=python3 test
make clean
pip uninstall -y uv
- name: VENV pre-installed with pip
run: |
python -m venv existingvenv
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 (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 test
make VENV_ENABLED=true VENV_CREATE=false VENV_FOLDER=existingvenv PRIMARY_PYTHON=python3 test
make clean
rm -r existingvenv
- name: VENV pre-installed with uv (global UV auto-detected)
- name: Global Python with pip
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 test
make VENV_ENABLED=false VENV_CREATE=false PRIMARY_PYTHON=python3 test
make clean
pip uninstall -y uv
rm -r existingvenv
- name: VENV with global UV using different UV_PYTHON

uv-variant:
strategy:
fail-fast: false
matrix:
# lowest and highest supported versions
python-version: ["3.10", "3.14"]
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v6
- name: Set up uv
uses: astral-sh/setup-uv@v5
- name: VENV created with uv
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 VENV_ENABLED=true VENV_CREATE=true PRIMARY_PYTHON=python3 UV_PYTHON=${{ matrix.python-version }} test
make clean
pip uninstall -y uv
- name: Global Python with pip
- name: VENV pre-installed with uv
run: |
make VENV_ENABLED=false VENV_CREATE=false PRIMARY_PYTHON=python3 PYTHON_PACKAGE_INSTALLER=pip test
uv venv -p ${{ matrix.python-version }} --seed existingvenv
make VENV_ENABLED=true VENV_CREATE=false VENV_FOLDER=existingvenv UV_PYTHON=${{ matrix.python-version }} test
make clean
rm -r existingvenv

uv-only:
name: UV-only (no Python) - Python ${{ matrix.uv-python }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ __pycache__
/.ruff_cache/
/.venv/
/.vscode/
/.worktrees/
/build/
/constraints-mxdev.txt
/dist/
Expand Down
10 changes: 10 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 3.0.0

- Split `core.mxenv` into a shared base plus `core.mxenv-pip` and `core.mxenv-uv`
environment variants. The package installer is now chosen by selecting a variant
instead of the removed `PYTHON_PACKAGE_INSTALLER` setting. QA dev-tool execution
runs through environment-provided macros; the uv variant runs style/format tools
(ruff, isort, black, zpretty, pyupgrade) via `uvx` by default. Type-checkers always
install into the environment. See the migration guide.
[jensens, 2026-05-30]

## 2.2.0

- Feature: Add optional `ruff check --fix` support to ruff-format target
Expand Down
185 changes: 83 additions & 102 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#: core.base
#: core.help
#: core.mxenv
#: core.mxenv-uv
#: core.mxfiles
#: core.packages
#: docs.sphinx
Expand Down Expand Up @@ -51,8 +52,7 @@ PROJECT_PATH_PYTHON?=

# Primary Python interpreter to use. It is used to create the
# virtual environment if `VENV_ENABLED` and `VENV_CREATE` are set to `true`.
# If global `uv` is used, this value is passed as `--python VALUE` to the venv creation.
# uv then downloads the Python interpreter if it is not available.
# If the uv variant is used, this value is the default for `UV_PYTHON`.
# for more on this feature read the [uv python documentation](https://docs.astral.sh/uv/concepts/python-versions/)
# Default: python3
PRIMARY_PYTHON?=3.14
Expand All @@ -61,21 +61,6 @@ PRIMARY_PYTHON?=3.14
# Default: 3.10
PYTHON_MIN_VERSION?=3.10

# Install packages using the given package installer method.
# 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

# 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.
# Default: true
Expand Down Expand Up @@ -103,6 +88,26 @@ MXDEV?=mxdev@git+https://github.com/mxstack/mxdev.git
# Default: mxmake
MXMAKE?=-e .

## core.mxenv-uv

# Python version for uv to install/use when creating the virtual
# environment. Passed to `uv venv -p VALUE`. Supports specs like `3.11`,
# `3.14`, `cpython@3.14`.
# Default: $(PRIMARY_PYTHON)
UV_PYTHON?=$(PRIMARY_PYTHON)

# How uv provisions the environment. `pip` (default) installs
# mxdev/mxmake with `uv pip install` (works for existing projects). `sync`
# uses `uv sync` against pyproject.toml/uv.lock (project-native, new projects).
# Default: pip
UV_PROVISION?=pip

# How style/format QA tools (ruff, isort, ...) are executed.
# `uvx` (default) runs them ephemerally without installing into the venv.
# `venv` installs them into the environment with `uv pip install`.
# Default: uvx
TOOL_EXECUTION?=uvx

## qa.ruff

# Source folder to scan for Python files to run ruff on.
Expand All @@ -119,12 +124,6 @@ RUFF_FIXES?=false
# Default: false
RUFF_UNSAFE_FIXES?=false

## qa.isort

# Source folder to scan for Python files to run isort on.
# Default: src
ISORT_SRC?=src

## docs.sphinx

# Documentation source folder.
Expand Down Expand Up @@ -246,82 +245,20 @@ else
MXENV_PYTHON=$(PRIMARY_PYTHON)
endif

# Determine the package installer with non-interactive flags
ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv")
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
##############################################################################
# Environment provisioning interface
##############################################################################

# The selected environment variant (core.mxenv-pip or core.mxenv-uv) provides:
# - PYTHON_PACKAGE_INSTALLER (pip|uv)
# - PYTHON_PACKAGE_COMMAND (installer command)
# - the $(MXENV_TARGET) rule (creates the environment)
# - INSTALL_TOOL / RUN_TOOL / UNINSTALL_TOOL macro definitions controlling how
# QA dev tools (ruff, isort, ...) are installed and invoked:
# INSTALL_TOOL(name) -> shell command to make the tool available
# RUN_TOOL(name,version,args) -> shell command to invoke the tool
# UNINSTALL_TOOL(name) -> shell command to remove the tool
MXENV_TARGET:=$(SENTINEL_FOLDER)/mxenv.sentinel
$(MXENV_TARGET): $(SENTINEL)
# 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 "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 || :
# 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 ("$(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)
@$(MXENV_PYTHON) -m ensurepip -U
endif
endif
else
@echo "Using system Python interpreter"
endif

# 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)
@touch $(MXENV_TARGET)

.PHONY: mxenv
mxenv: $(MXENV_TARGET)
Expand All @@ -345,6 +282,50 @@ INSTALL_TARGETS+=mxenv
DIRTY_TARGETS+=mxenv-dirty
CLEAN_TARGETS+=mxenv-clean

##############################################################################
# mxenv-uv
##############################################################################

# Internal: identify the active installer for downstream domains.
PYTHON_PACKAGE_INSTALLER:=uv
PYTHON_PACKAGE_COMMAND=uv pip --no-progress

# Tool execution depends on TOOL_EXECUTION.
ifeq ("$(TOOL_EXECUTION)","uvx")
INSTALL_TOOL=:
RUN_TOOL=uvx $(1)$(if $(strip $(2)),==$(strip $(2)),) $(3)
UNINSTALL_TOOL=:
else
INSTALL_TOOL=$(PYTHON_PACKAGE_COMMAND) install $(1)
RUN_TOOL=$(1) $(3)
UNINSTALL_TOOL=uv pip uninstall $(1) || :
endif

$(MXENV_TARGET): $(SENTINEL)
# Validation: uv must be available
@command -v uv >/dev/null 2>&1 \
|| { echo "uv not found. Install uv or switch to core.mxenv-pip."; exit 1; }
# Validation: VENV_FOLDER set when venv enabled
@[[ "$(VENV_ENABLED)" == "true" && "$(VENV_FOLDER)" == "" ]] \
&& echo "VENV_FOLDER must be configured if VENV_ENABLED is true" && exit 1 || :
ifeq ("$(VENV_ENABLED)", "true")
ifeq ("$(VENV_CREATE)", "true")
@echo "Setup Python Virtual Environment using uv at '$(VENV_FOLDER)'"
@uv venv --allow-existing --no-progress -p $(UV_PYTHON) --seed $(VENV_FOLDER)
endif
else
@echo "Using system Python interpreter"
endif
ifeq ("$(UV_PROVISION)","sync")
@echo "Provision environment with uv sync"
@uv sync
else
@$(PYTHON_PACKAGE_COMMAND) install -U pip setuptools wheel
@echo "Install/Update MXStack Python packages"
@$(PYTHON_PACKAGE_COMMAND) install -U $(MXDEV) $(MXMAKE)
endif
@touch $(MXENV_TARGET)

##############################################################################
# ruff
##############################################################################
Expand All @@ -366,21 +347,21 @@ endif
RUFF_TARGET:=$(SENTINEL_FOLDER)/ruff.sentinel
$(RUFF_TARGET): $(MXENV_TARGET)
@echo "Install Ruff"
@$(PYTHON_PACKAGE_COMMAND) install ruff
@$(call INSTALL_TOOL,ruff)
@touch $(RUFF_TARGET)

.PHONY: ruff-check
ruff-check: $(RUFF_TARGET)
@echo "Run ruff check"
@ruff check $(RUFF_SRC)
@$(call RUN_TOOL,ruff,,check $(RUFF_SRC))

.PHONY: ruff-format
ruff-format: $(RUFF_TARGET)
@echo "Run ruff format"
@ruff format $(RUFF_SRC)
@$(call RUN_TOOL,ruff,,format $(RUFF_SRC))
ifeq ("$(RUFF_FIXES)","true")
@echo "Run ruff check $(RUFF_FIX_FLAGS)"
@ruff check $(RUFF_FIX_FLAGS) $(RUFF_SRC)
@$(call RUN_TOOL,ruff,,check $(RUFF_FIX_FLAGS) $(RUFF_SRC))
endif

.PHONY: ruff-dirty
Expand All @@ -389,7 +370,7 @@ ruff-dirty:

.PHONY: ruff-clean
ruff-clean: ruff-dirty
@test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y ruff || :
@$(call UNINSTALL_TOOL,ruff)
@rm -rf .ruff_cache

INSTALL_TARGETS+=$(RUFF_TARGET)
Expand Down
Loading
Loading