Skip to content
Open
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
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
9 changes: 9 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 2.2.1 (unreleased)

- Fix: `make install` now honors pinned versions from `constraints.txt` /
`constraints-mxdev.txt`. All `pip install` recipes in topic templates pass
`$(PIP_CONSTRAINTS_FLAG)` and no longer use `-U`, preventing transitive
dependencies from being silently upgraded past project pins.
Resolves [#70](https://github.com/mxstack/mxmake/issues/70).
[jensens, 2026-04-27]

## 2.2.0

- Feature: Add optional `ruff check --fix` support to ruff-format target
Expand Down
26 changes: 14 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,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 @@ -253,6 +247,13 @@ else
PYTHON_PACKAGE_COMMAND=$(MXENV_PYTHON) -m pip
endif

# Constraints file used by pip install recipes. Falls back to the project's
# own constraints.txt during bootstrap (before mxdev generates the rewritten
# constraints-mxdev.txt). Lazy assignment so $(wildcard) re-evaluates on use.
MXDEV_CONSTRAINTS?=constraints-mxdev.txt
PROJECT_CONSTRAINTS:=$(PYTHON_PROJECT_PREFIX)constraints.txt
PIP_CONSTRAINTS_FLAG=$(if $(wildcard $(MXDEV_CONSTRAINTS)),-c $(MXDEV_CONSTRAINTS),$(if $(wildcard $(PROJECT_CONSTRAINTS)),-c $(PROJECT_CONSTRAINTS),))

# 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")
Expand Down Expand Up @@ -318,9 +319,9 @@ ifeq ("$(USE_LOCAL_UV)","true")
endif

# Install/upgrade core packages
@$(PYTHON_PACKAGE_COMMAND) install -U pip setuptools wheel
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) pip setuptools wheel
@echo "Install/Update MXStack Python packages"
@$(PYTHON_PACKAGE_COMMAND) install -U $(MXDEV) $(MXMAKE)
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) $(MXDEV) $(MXMAKE)
@touch $(MXENV_TARGET)

.PHONY: mxenv
Expand Down Expand Up @@ -366,7 +367,7 @@ endif
RUFF_TARGET:=$(SENTINEL_FOLDER)/ruff.sentinel
$(RUFF_TARGET): $(MXENV_TARGET)
@echo "Install Ruff"
@$(PYTHON_PACKAGE_COMMAND) install ruff
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) ruff
@touch $(RUFF_TARGET)

.PHONY: ruff-check
Expand Down Expand Up @@ -411,7 +412,7 @@ SPHINX_AUTOBUILD_BIN=sphinx-autobuild
DOCS_TARGET:=$(SENTINEL_FOLDER)/sphinx.sentinel
$(DOCS_TARGET): $(MXENV_TARGET)
@echo "Install Sphinx"
@$(PYTHON_PACKAGE_COMMAND) install -U sphinx sphinx-autobuild $(DOCS_REQUIREMENTS)
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) sphinx sphinx-autobuild $(DOCS_REQUIREMENTS)
@touch $(DOCS_TARGET)

.PHONY: docs
Expand Down Expand Up @@ -520,6 +521,7 @@ endif
PACKAGES_TARGET:=$(INSTALLED_PACKAGES)
$(PACKAGES_TARGET): $(FILES_TARGET) $(ADDITIONAL_SOURCES_TARGETS)
@echo "Install python packages"
# constraints already embedded in $(FILES_TARGET) via mxdev's `-c` line
@$(PYTHON_PACKAGE_COMMAND) install $(PACKAGES_PRERELEASES) -r $(FILES_TARGET)
@$(PYTHON_PACKAGE_COMMAND) freeze > $(INSTALLED_PACKAGES)
@touch $(PACKAGES_TARGET)
Expand Down Expand Up @@ -561,7 +563,7 @@ endif
TY_TARGET:=$(SENTINEL_FOLDER)/ty.sentinel
$(TY_TARGET): $(MXENV_TARGET)
@echo "Install ty"
@$(PYTHON_PACKAGE_COMMAND) install ty
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) ty
@touch $(TY_TARGET)

.PHONY: ty
Expand Down Expand Up @@ -591,7 +593,7 @@ DIRTY_TARGETS+=ty-dirty
TEST_TARGET:=$(SENTINEL_FOLDER)/test.sentinel
$(TEST_TARGET): $(MXENV_TARGET)
@echo "Install $(TEST_REQUIREMENTS)"
@$(PYTHON_PACKAGE_COMMAND) install $(TEST_REQUIREMENTS)
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) $(TEST_REQUIREMENTS)
@touch $(TEST_TARGET)

.PHONY: test
Expand Down
16 changes: 16 additions & 0 deletions docs/source/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

This guide documents breaking changes between mxmake versions and how to migrate your projects.

## Version 2.2.1 (unreleased)

### Behavior Change: `pip install` recipes now honor pinned constraints

**What changed**: Topic templates no longer pass `-U` to `pip install` and now route through a new lazy-evaluated `PIP_CONSTRAINTS_FLAG` Makefile variable that resolves to `-c constraints-mxdev.txt` (or `-c constraints.txt` during bootstrap, if present). Previously, `make install` could silently upgrade transitive dependencies past pins because individual tool installs (sphinx, coverage, ruff, etc.) ignored constraint files.

**Why**: Local venvs were diverging from CI builds, breaking reproducibility for projects that rely on pin discipline. See issue [#70](https://github.com/mxstack/mxmake/issues/70).

**Migration**:

- Run `mxmake update` to regenerate your `Makefile` with the new install recipes.
- If you intentionally relied on the old "upgrade everything to latest on every install" behavior, run `make clean && make install` after bumping pins to force a refresh.
- No other action required. Generated Makefiles continue to work without a `constraints.txt` — the new flag falls back to empty in that case.

**No breaking changes**: All settings, targets, and CLI commands are unchanged.

## Version 2.0.1 (unreleased)

### Added: Monorepo Support
Expand Down
11 changes: 9 additions & 2 deletions src/mxmake/tests/expected/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ else
PYTHON_PACKAGE_COMMAND=$(MXENV_PYTHON) -m pip
endif

# Constraints file used by pip install recipes. Falls back to the project's
# own constraints.txt during bootstrap (before mxdev generates the rewritten
# constraints-mxdev.txt). Lazy assignment so $(wildcard) re-evaluates on use.
MXDEV_CONSTRAINTS?=constraints-mxdev.txt
PROJECT_CONSTRAINTS:=$(PYTHON_PROJECT_PREFIX)constraints.txt
PIP_CONSTRAINTS_FLAG=$(if $(wildcard $(MXDEV_CONSTRAINTS)),-c $(MXDEV_CONSTRAINTS),$(if $(wildcard $(PROJECT_CONSTRAINTS)),-c $(PROJECT_CONSTRAINTS),))

# 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")
Expand Down Expand Up @@ -222,9 +229,9 @@ ifeq ("$(USE_LOCAL_UV)","true")
endif

# Install/upgrade core packages
@$(PYTHON_PACKAGE_COMMAND) install -U pip setuptools wheel
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) pip setuptools wheel
@echo "Install/Update MXStack Python packages"
@$(PYTHON_PACKAGE_COMMAND) install -U $(MXDEV) $(MXMAKE)
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) $(MXDEV) $(MXMAKE)
@touch $(MXENV_TARGET)

.PHONY: mxenv
Expand Down
2 changes: 1 addition & 1 deletion src/mxmake/topics/applications/cookiecutter.mk
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
COOKIECUTTER_TARGET:=$(SENTINEL_FOLDER)/cookiecutter.sentinel
$(COOKIECUTTER_TARGET): $(MXENV_TARGET)
@echo "Install cookiecutter"
@$(PYTHON_PACKAGE_COMMAND) install "cookiecutter>=2.6.0"
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) "cookiecutter>=2.6.0"
@touch $(COOKIECUTTER_TARGET)

.PHONY: cookiecutter
Expand Down
2 changes: 1 addition & 1 deletion src/mxmake/topics/applications/twisted.mk
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
TWISTED_TARGET:=$(SENTINEL_FOLDER)/twisted.sentinel
$(TWISTED_TARGET): $(MXENV_TARGET)
@echo "Install twisted"
@$(PYTHON_PACKAGE_COMMAND) install Twisted
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) Twisted
@touch $(TWISTED_TARGET)

.PHONY: twisted-start
Expand Down
2 changes: 1 addition & 1 deletion src/mxmake/topics/applications/zest-releaser.mk
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
ZEST_RELEASER_TARGET:=$(SENTINEL_FOLDER)/zest-releaser.sentinel
$(ZEST_RELEASER_TARGET): $(MXENV_TARGET)
@echo "Install zest.releaser"
@$(PYTHON_PACKAGE_COMMAND) install zest.releaser
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) zest.releaser
@touch $(ZEST_RELEASER_TARGET)

.PHONY: zest-releaser-prerelease
Expand Down
11 changes: 9 additions & 2 deletions src/mxmake/topics/core/mxenv.mk
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ else
PYTHON_PACKAGE_COMMAND=$(MXENV_PYTHON) -m pip
endif

# Constraints file used by pip install recipes. Falls back to the project's
# own constraints.txt during bootstrap (before mxdev generates the rewritten
# constraints-mxdev.txt). Lazy assignment so $(wildcard) re-evaluates on use.
MXDEV_CONSTRAINTS?=constraints-mxdev.txt
PROJECT_CONSTRAINTS:=$(PYTHON_PROJECT_PREFIX)constraints.txt
PIP_CONSTRAINTS_FLAG=$(if $(wildcard $(MXDEV_CONSTRAINTS)),-c $(MXDEV_CONSTRAINTS),$(if $(wildcard $(PROJECT_CONSTRAINTS)),-c $(PROJECT_CONSTRAINTS),))

# 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")
Expand Down Expand Up @@ -167,9 +174,9 @@ ifeq ("$(USE_LOCAL_UV)","true")
endif

# Install/upgrade core packages
@$(PYTHON_PACKAGE_COMMAND) install -U pip setuptools wheel
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) pip setuptools wheel
@echo "Install/Update MXStack Python packages"
@$(PYTHON_PACKAGE_COMMAND) install -U $(MXDEV) $(MXMAKE)
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) $(MXDEV) $(MXMAKE)
@touch $(MXENV_TARGET)

.PHONY: mxenv
Expand Down
1 change: 1 addition & 0 deletions src/mxmake/topics/core/packages.mk
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ endif
PACKAGES_TARGET:=$(INSTALLED_PACKAGES)
$(PACKAGES_TARGET): $(FILES_TARGET) $(ADDITIONAL_SOURCES_TARGETS)
@echo "Install python packages"
# constraints already embedded in $(FILES_TARGET) via mxdev's `-c` line
@$(PYTHON_PACKAGE_COMMAND) install $(PACKAGES_PRERELEASES) -r $(FILES_TARGET)
@$(PYTHON_PACKAGE_COMMAND) freeze > $(INSTALLED_PACKAGES)
@touch $(PACKAGES_TARGET)
Expand Down
2 changes: 1 addition & 1 deletion src/mxmake/topics/docs/sphinx.mk
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ SPHINX_AUTOBUILD_BIN=sphinx-autobuild
DOCS_TARGET:=$(SENTINEL_FOLDER)/sphinx.sentinel
$(DOCS_TARGET): $(MXENV_TARGET)
@echo "Install Sphinx"
@$(PYTHON_PACKAGE_COMMAND) install -U sphinx sphinx-autobuild $(DOCS_REQUIREMENTS)
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) sphinx sphinx-autobuild $(DOCS_REQUIREMENTS)
@touch $(DOCS_TARGET)

.PHONY: docs
Expand Down
2 changes: 1 addition & 1 deletion src/mxmake/topics/i18n/lingua.mk
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
LINGUA_TARGET:=$(SENTINEL_FOLDER)/lingua.sentinel
$(LINGUA_TARGET): $(MXENV_TARGET)
@echo "Install Lingua"
@$(PYTHON_PACKAGE_COMMAND) install chameleon lingua $(LINGUA_PLUGINS)
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) chameleon lingua $(LINGUA_PLUGINS)
@touch $(LINGUA_TARGET)

PHONY: lingua-extract
Expand Down
1 change: 1 addition & 0 deletions src/mxmake/topics/ldap/python-ldap.mk
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ SYSTEM_DEPENDENCIES+=python3-dev libldap2-dev libssl-dev libsasl2-dev
PYTHON_LDAP_TARGET:=$(SENTINEL_FOLDER)/python-ldap.sentinel
$(PYTHON_LDAP_TARGET): $(MXENV_TARGET) $(OPENLDAP_TARGET)
@$(PYTHON_PACKAGE_COMMAND) install \
$(PIP_CONSTRAINTS_FLAG) \
--force-reinstall \
python-ldap
@touch $(PYTHON_LDAP_TARGET)
Expand Down
2 changes: 1 addition & 1 deletion src/mxmake/topics/qa/black.mk
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ endif
BLACK_TARGET:=$(SENTINEL_FOLDER)/black.sentinel
$(BLACK_TARGET): $(MXENV_TARGET)
@echo "Install Black"
@$(PYTHON_PACKAGE_COMMAND) install black
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) black
@touch $(BLACK_TARGET)

.PHONY: black-check
Expand Down
2 changes: 1 addition & 1 deletion src/mxmake/topics/qa/coverage.mk
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
COVERAGE_TARGET:=$(SENTINEL_FOLDER)/coverage.sentinel
$(COVERAGE_TARGET): $(TEST_TARGET)
@echo "Install Coverage"
@$(PYTHON_PACKAGE_COMMAND) install -U coverage
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) coverage
@touch $(COVERAGE_TARGET)

.PHONY: coverage
Expand Down
2 changes: 1 addition & 1 deletion src/mxmake/topics/qa/isort.mk
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ endif
ISORT_TARGET:=$(SENTINEL_FOLDER)/isort.sentinel
$(ISORT_TARGET): $(MXENV_TARGET)
@echo "Install isort"
@$(PYTHON_PACKAGE_COMMAND) install isort
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) isort
@touch $(ISORT_TARGET)

.PHONY: isort-check
Expand Down
2 changes: 1 addition & 1 deletion src/mxmake/topics/qa/mypy.mk
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ endif
MYPY_TARGET:=$(SENTINEL_FOLDER)/mypy.sentinel
$(MYPY_TARGET): $(MXENV_TARGET)
@echo "Install mypy"
@$(PYTHON_PACKAGE_COMMAND) install mypy $(MYPY_REQUIREMENTS)
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) mypy $(MYPY_REQUIREMENTS)
@touch $(MYPY_TARGET)

.PHONY: mypy
Expand Down
2 changes: 1 addition & 1 deletion src/mxmake/topics/qa/pyrefly.mk
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ endif
PYREFLY_TARGET:=$(SENTINEL_FOLDER)/pyrefly.sentinel
$(PYREFLY_TARGET): $(MXENV_TARGET)
@echo "Install pyrefly"
@$(PYTHON_PACKAGE_COMMAND) install pyrefly $(PYREFLY_REQUIREMENTS)
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) pyrefly $(PYREFLY_REQUIREMENTS)
@touch $(PYREFLY_TARGET)

.PHONY: pyrefly
Expand Down
2 changes: 1 addition & 1 deletion src/mxmake/topics/qa/pyupgrade.mk
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ endif
PYUPGRADE_TARGET:=$(SENTINEL_FOLDER)/pyupgrade.sentinel
$(PYUPGRADE_TARGET): $(MXENV_TARGET)
@echo "Install pyupgrade"
@$(PYTHON_PACKAGE_COMMAND) install pyupgrade
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) pyupgrade
@touch $(PYUPGRADE_TARGET)

.PHONY: pyupgrade-format
Expand Down
2 changes: 1 addition & 1 deletion src/mxmake/topics/qa/ruff.mk
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ endif
RUFF_TARGET:=$(SENTINEL_FOLDER)/ruff.sentinel
$(RUFF_TARGET): $(MXENV_TARGET)
@echo "Install Ruff"
@$(PYTHON_PACKAGE_COMMAND) install ruff
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) ruff
@touch $(RUFF_TARGET)

.PHONY: ruff-check
Expand Down
2 changes: 1 addition & 1 deletion src/mxmake/topics/qa/test.mk
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
TEST_TARGET:=$(SENTINEL_FOLDER)/test.sentinel
$(TEST_TARGET): $(MXENV_TARGET)
@echo "Install $(TEST_REQUIREMENTS)"
@$(PYTHON_PACKAGE_COMMAND) install $(TEST_REQUIREMENTS)
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) $(TEST_REQUIREMENTS)
@touch $(TEST_TARGET)

.PHONY: test
Expand Down
2 changes: 1 addition & 1 deletion src/mxmake/topics/qa/ty.mk
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ endif
TY_TARGET:=$(SENTINEL_FOLDER)/ty.sentinel
$(TY_TARGET): $(MXENV_TARGET)
@echo "Install ty"
@$(PYTHON_PACKAGE_COMMAND) install ty
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) ty
@touch $(TY_TARGET)

.PHONY: ty
Expand Down
2 changes: 1 addition & 1 deletion src/mxmake/topics/qa/zpretty.mk
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ endif
ZPRETTY_TARGET:=$(SENTINEL_FOLDER)/zpretty.sentinel
$(ZPRETTY_TARGET): $(MXENV_TARGET)
@echo "Install zpretty"
@$(PYTHON_PACKAGE_COMMAND) install zpretty
@$(PYTHON_PACKAGE_COMMAND) install $(PIP_CONSTRAINTS_FLAG) zpretty
@touch $(ZPRETTY_TARGET)

.PHONY: zpretty-check
Expand Down
Loading