Spaces:
Sleeping
Sleeping
# | |
# 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 | |
# <http://www.apache.org/licenses/LICENSE-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,$@) |