# # SEAMLESSLY MANAGE PYTHON VIRTUAL ENVIRONMENT WITH A MAKEFILE # # https://github.com/sio/Makefile.venv v2023.04.17 # # # Insert `include Makefile.venv` at the bottom of your Makefile to enable these # rules. # # When writing your Makefile use '$(VENV)/python' to refer to the Python # interpreter within virtual environment and '$(VENV)/executablename' for any # other executable in venv. # # This Makefile provides the following targets: # venv # Use this as a dependency for any target that requires virtual # environment to be created and configured # python, ipython # Use these to launch interactive Python shell within virtual environment # shell, bash, zsh # Launch interactive command line shell. "shell" target launches the # default shell Makefile executes its rules in (usually /bin/sh). # "bash" and "zsh" can be used to refer to the specific desired shell. # show-venv # Show versions of Python and pip, and the path to the virtual environment # clean-venv # Remove virtual environment # $(VENV)/executable_name # Install `executable_name` with pip. Only packages with names matching # the name of the corresponding executable are supported. # Use this as a lightweight mechanism for development dependencies # tracking. E.g. for one-off tools that are not required in every # developer's environment, therefore are not included into # requirements.txt or setup.py. # Note: # Rules using such target or dependency MUST be defined below # `include` directive to make use of correct $(VENV) value. # Example: # codestyle: $(VENV)/pyflakes # $(VENV)/pyflakes . # See `ipython` target below for another example. # # This Makefile can be configured via following variables: # PY # Command name for system Python interpreter. It is used only initially to # create the virtual environment # Default: python3 # REQUIREMENTS_TXT # Space separated list of paths to requirements.txt files. # Paths are resolved relative to current working directory. # Default: requirements.txt # # Non-existent files are treated as hard dependencies, # recipes for creating such files must be provided by the main Makefile. # Providing empty value (REQUIREMENTS_TXT=) turns off processing of # requirements.txt even when the file exists. # SETUP_PY, SETUP_CFG, PYPROJECT_TOML, VENV_LOCAL_PACKAGE # Space separated list of paths to files that contain build instructions # for local Python packages. Corresponding packages will be installed # into venv in editable mode along with all their dependencies. # Default: setup.py setup.cfg pyproject.toml (whichever present) # # Non-existent and empty values are treated in the same way as for REQUIREMENTS_TXT. # WORKDIR # Parent directory for the virtual environment. # Default: current working directory. # VENVDIR # Python virtual environment directory. # Default: $(WORKDIR)/.venv # # This Makefile was written for GNU Make and may not work with other make # implementations. # # # Copyright (c) 2019-2023 Vitaly Potyarkin # # Licensed under the Apache License, Version 2.0 # # # # Configuration variables # WORKDIR?=. VENVDIR?=$(WORKDIR)/.venv REQUIREMENTS_TXT?=$(wildcard requirements.txt) # Multiple paths are supported (space separated) SETUP_PY?=$(wildcard setup.py) # Multiple paths are supported (space separated) SETUP_CFG?=$(foreach s,$(SETUP_PY),$(wildcard $(patsubst %setup.py,%setup.cfg,$(s)))) PYPROJECT_TOML?=$(wildcard pyproject.toml) VENV_LOCAL_PACKAGE?=$(SETUP_PY) $(SETUP_CFG) $(PYPROJECT_TOML) MARKER=.initialized-with-Makefile.venv # # Python interpreter detection # _PY_AUTODETECT_MSG=Detected Python interpreter: $(PY). Use PY environment variable to override ifeq (ok,$(shell test -e /dev/null 2>&1 && echo ok)) NULL_STDERR=2>/dev/null else NULL_STDERR=2>NUL endif ifndef PY _PY_OPTION:=python3 ifeq (ok,$(shell $(_PY_OPTION) -c "print('ok')" $(NULL_STDERR))) PY=$(_PY_OPTION) endif endif ifndef PY _PY_OPTION:=$(VENVDIR)/bin/python ifeq (ok,$(shell $(_PY_OPTION) -c "print('ok')" $(NULL_STDERR))) PY=$(_PY_OPTION) $(info $(_PY_AUTODETECT_MSG)) endif endif ifndef PY _PY_OPTION:=$(subst /,\,$(VENVDIR)/Scripts/python) ifeq (ok,$(shell $(_PY_OPTION) -c "print('ok')" $(NULL_STDERR))) PY=$(_PY_OPTION) $(info $(_PY_AUTODETECT_MSG)) endif endif ifndef PY _PY_OPTION:=py -3 ifeq (ok,$(shell $(_PY_OPTION) -c "print('ok')" $(NULL_STDERR))) PY=$(_PY_OPTION) $(info $(_PY_AUTODETECT_MSG)) endif endif ifndef PY _PY_OPTION:=python ifeq (ok,$(shell $(_PY_OPTION) -c "print('ok')" $(NULL_STDERR))) PY=$(_PY_OPTION) $(info $(_PY_AUTODETECT_MSG)) endif endif ifndef PY define _PY_AUTODETECT_ERR Could not detect Python interpreter automatically. Please specify path to interpreter via PY environment variable. endef $(error $(_PY_AUTODETECT_ERR)) endif # # Internal variable resolution # VENV=$(VENVDIR)/bin EXE= # Detect windows ifeq (win32,$(shell $(PY) -c "import __future__, sys; print(sys.platform)")) VENV=$(VENVDIR)/Scripts EXE=.exe endif touch=touch $(1) ifeq (,$(shell command -v touch $(NULL_STDERR))) # https://ss64.com/nt/touch.html touch=type nul >> $(subst /,\,$(1)) && copy /y /b $(subst /,\,$(1))+,, $(subst /,\,$(1)) endif RM?=rm -f ifeq (,$(shell command -v $(firstword $(RM)) $(NULL_STDERR))) RMDIR:=rd /s /q else RMDIR:=$(RM) -r endif # # Virtual environment # .PHONY: venv venv: $(VENV)/$(MARKER) .PHONY: clean-venv clean-venv: -$(RMDIR) "$(VENVDIR)" .PHONY: show-venv show-venv: venv @$(VENV)/python -c "import sys; print('Python ' + sys.version.replace('\n',''))" @$(VENV)/pip --version @echo venv: $(VENVDIR) .PHONY: debug-venv debug-venv: @echo "PATH (Shell)=$$PATH" @$(MAKE) --version $(info PATH (GNU Make)="$(PATH)") $(info SHELL="$(SHELL)") $(info PY="$(PY)") $(info REQUIREMENTS_TXT="$(REQUIREMENTS_TXT)") $(info VENV_LOCAL_PACKAGE="$(VENV_LOCAL_PACKAGE)") $(info VENVDIR="$(VENVDIR)") $(info VENVDEPENDS="$(VENVDEPENDS)") $(info WORKDIR="$(WORKDIR)") # # Dependencies # ifneq ($(strip $(REQUIREMENTS_TXT)),) VENVDEPENDS+=$(REQUIREMENTS_TXT) endif ifneq ($(strip $(VENV_LOCAL_PACKAGE)),) VENVDEPENDS+=$(VENV_LOCAL_PACKAGE) endif $(VENV): $(PY) -m venv $(VENVDIR) $(VENV)/python -m pip install --upgrade pip setuptools wheel $(VENV)/$(MARKER): $(VENVDEPENDS) | $(VENV) ifneq ($(strip $(REQUIREMENTS_TXT)),) $(VENV)/pip install $(foreach path,$(REQUIREMENTS_TXT),-r $(path)) endif ifneq ($(strip $(VENV_LOCAL_PACKAGE)),) $(VENV)/pip install $(foreach path,$(sort $(VENV_LOCAL_PACKAGE)),-e $(dir $(path))) endif $(call touch,$(VENV)/$(MARKER)) # # Interactive shells # .PHONY: python python: venv exec $(VENV)/python .PHONY: ipython ipython: $(VENV)/ipython exec $(VENV)/ipython .PHONY: shell shell: venv . $(VENV)/activate && exec $(notdir $(SHELL)) .PHONY: bash zsh bash zsh: venv . $(VENV)/activate && exec $@ # # Commandline tools (wildcard rule, executable name must match package name) # ifneq ($(EXE),) $(VENV)/%: $(VENV)/%$(EXE) ; .PHONY: $(VENV)/% .PRECIOUS: $(VENV)/%$(EXE) endif $(VENV)/%$(EXE): $(VENV)/$(MARKER) $(VENV)/pip install --upgrade $* $(call touch,$@)