Skip to content

Commit

Permalink
build: optimize the development environment configuration guidance
Browse files Browse the repository at this point in the history
  • Loading branch information
xenophonf committed Sep 9, 2024
1 parent c7e6052 commit a917e48
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 117 deletions.
44 changes: 24 additions & 20 deletions .dir-locals.el
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
;;; Directory Local Variables -*- no-byte-compile: t -*-
;;; For more information see (info "(emacs) Directory Variables")

((nil . ((eval . (progn
;; install or activate the development environment
;; (requires pyvenv-tracking-mode)
(set (make-local-variable 'my-project)
(locate-dominating-file default-directory ".dir-locals.el"))
(set (make-local-variable 'my-project-venv)
(concat my-project ".venv"))
(if (not (file-exists-p my-project-venv))
(let ((cwd default-directory))
(cd my-project)
(async-shell-command "make dev-infra")
(cd cwd)
(message "Please re-open this file/directory after the \"make dev-infra\" command finishes."))
;; must be set project-wide to pre-commit work
(set (make-local-variable 'pyvenv-activate)
my-project-venv))))))
(python-mode . ((eval . (progn
;; sort imports, then style code
(add-hook 'before-save-hook #'py-isort-before-save nil t)
(add-hook 'before-save-hook #'elpy-black-fix-code nil t))))))
((nil
(eval progn
;; install or activate the development environment
;; (requires pyvenv-tracking-mode)
(set (make-local-variable 'my-project)
(locate-dominating-file default-directory ".dir-locals.el"))
(set (make-local-variable 'my-project-venv)
(concat my-project ".venv"))
(if (not (file-exists-p my-project-venv))
(let ((cwd default-directory)
(cmd "make setup"))
(cd my-project)
(async-shell-command cmd)
(cd cwd)
(message
(format "Please re-open this file/directory after the \"%s\" command finishes." cmd)))
;; must be set project-wide for pre-commit to work
(set (make-local-variable 'pyvenv-activate)
my-project-venv))))
(python-mode
(eval progn
;; sort imports, then style code
(add-hook 'before-save-hook #'py-isort-before-save nil t)
(add-hook 'before-save-hook #'elpy-black-fix-code nil t))))))
35 changes: 28 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,42 @@ This project uses the [Git feature branch workflow](https://www.atlassian.com/gi

## Development Environment

This project requires Python 3.10 or newer. To set up your development environment on Linux, run these commands from the project root directory:
This project requires Python 3.10 or newer. To set up your
development environment on Linux, run these commands from the project
root directory:

- `sudo make build-deps`—installs build dependencies (Debian/Ubuntu only)
- `sudo make build-deps`—installs development tools and build
dependencies on supported operating systems, e.g., Python

- `make`—creates a virtual environment named `.venv` in the current working directory and performs an editable installation of this project, including development and testing tools
- `make setup`—creates (or updates) a
[Python virtual environment](https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/#create-and-use-virtual-environments)
named `.venv` in the project root directory and performs an editable
installation of this project plus development and testing tools

- `make pre-commit`—installs pre-commit hooks (requires the virtual environment to be active in your code editor or [Git porcelain](https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain))
- `make pre-commit`—configures optional pre-commit hooks; requires the
virtual environment to be active in your code editor or
[Git porcelain](https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain)

- `make test`—performs comprehensive functional and integration testing of this project
- `make clean`—resets the development environment

- `sudo make clean-deps`—uninstalls development tools and build
dependencies on supported operating systems

Additional [make(1)](https://linux.die.net/man/1/make) targets are
available, several of which are listed below. Review the
[makefile](GNUmakefile) for details.

- `make lint`—check code syntax and style

- `make test`—performs comprehensive functional and integration
testing of this project

- `make smoke`—runs a shorter, faster subset of the test suite

- `make docker`—builds a fully tested and release-ready container image
- `make debug`—runs the Flask web app with debugging enabled

Additional [make(1)](https://linux.die.net/man/1/make) targets are available. Review the [Makefile](Makefile) for details.
- `make docker`—builds a fully tested and release-ready container
image

## Code Style

Expand Down
247 changes: 157 additions & 90 deletions GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,79 +15,130 @@
# License along with this program. If not, see
# <https://www.gnu.org/licenses/>.

.PHONY: dev-infra venv debug run smoke test tests tests coverage dist \
distcheck distclean pre-commit check checks list builder \
tester container docker prune bashbrew manifest-tool \
build-deps clean-deps clean

# Install Stuart in a virtual environment. (See also the build-deps target.)

PYV = $(shell python3 -c "import sys;print('{}.{}'.format(*sys.version_info[:2]))")

dev-infra: .venv/lib/python$(PYV)/site-packages/psycopg2.py \
.venv/bin/bashbrew .venv/bin/manifest-tool

.venv/lib/python$(PYV)/site-packages/psycopg2.py: stuart.egg-info
echo "from psycopg2cffi import compat\ncompat.register()" > $@

stuart.egg-info: .venv pyproject.toml src/*.py
. .venv/bin/activate; pip install -U pip setuptools
. .venv/bin/activate; pip install -e .[psycopg2cffi,dev,test]

venv: .venv

.venv:
python3 -m venv $@

debug: .venv/lib/python$(PYV)/site-packages/psycopg2.py
. .venv/bin/activate; python3 -m flask --debug --app stuart.app run $(ARGS)

run: .venv/lib/python$(PYV)/site-packages/psycopg2.py
. .venv/bin/activate; python3 -m flask --app stuart.app run $(ARGS)

smoke: .venv/lib/python$(PYV)/site-packages/psycopg2.py
. .venv/bin/activate; pytest -m "smoke and not slow"

test tests: .venv/lib/python$(PYV)/site-packages/psycopg2.py
. .venv/bin/activate; pytest

coverage: .venv/lib/python$(PYV)/site-packages/psycopg2.py
. .venv/bin/activate; pytest --cov=stuart

dist: .venv/lib/python$(PYV)/site-packages/psycopg2.py
# Search a colon-separated list of directories for one of the given
# programs, returning the first match.
pathsearch = \
$(or \
$(firstword \
$(foreach a, $(2), \
$(wildcard $(addsuffix /$(a), $(subst :, , $(1)))))), \
$(3))

# Search the Python virtual environment and the executable search path
# for the programs in the listed order, returning the first match.
venvsearch = \
$(if $(call pathsearch,.venv/bin,$(1)), \
. .venv/bin/activate; $(1), \
$(call pathsearch,$(PATH),$(1),exit 1; echo $(1)))

# Develop using the latest available supported version of Python.
PYTHON = \
$(call pathsearch,$(PATH),python3.12 python3.11 python3.10,exit 1; echo python3)
PYTHON_VERSION = \
$(shell $(PYTHON) -c "import sys;print('{}.{}'.format(*sys.version_info[:2]))")

# Use these tools from the development environment, if available.
PRE_COMMIT = $(call venvsearch,pre-commit)
PYTEST = $(call venvsearch,pytest)
TOMLQ = $(call venvsearch,tomlq)
YQ = $(call venvsearch,yq)

# Determine the host operating system.
UNAME = $(or $(shell uname))
LSB_RELEASE = $(call pathsearch,$(PATH),lsb_release,exit 1; echo lsb_release)
DISTRO = $(if $(filter Linux, $(UNAME)), $(or $(shell $(LSB_RELEASE) -is)))

# Use these settings when developing on Debian/Ubuntu.
APT_GET = \
apt-get -o Debug::pkgProblemResolver=yes -y --no-install-recommends
DEBIAN_BUILD_DEPS = \
build-essential \
devscripts \
equivs \
postgresql \

# Get the package name.
PYPACKAGE_NAME = \
$(shell $(TOMLQ) -r '.tool.setuptools."package-dir"|keys[0]' pyproject.toml)

# List in-use pre-commit hooks.
PRE_COMMIT_HOOKS = \
$(addprefix .git/hooks/, \
$(shell \
$(YQ) -r ".repos[].hooks[].stages[]" .pre-commit-config.yaml \
2>/dev/null \
| sort -u \
) \
pre-commit \
)

# When adding an alias for a build artifact, add it to this list; cf.
# https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html.
.PHONY: \
bashbrew \
build-deps \
clean \
clean-deps \
coverage \
debug \
dist \
distcheck \
distclean \
lint \
manifest-tool \
pre-commit \
run \
setup \
smoke \
test \
tests \
venv \

# Debug/run the web app.
debug: .coverage
. .venv/bin/activate; python -m flask --debug --app stuart.app run $(ARGS)

run: .coverage
. .venv/bin/activate; python -m flask --app stuart.app run $(ARGS)

# Build the distribution.
dist: .coverage
. .venv/bin/activate; python -m build

distcheck: dist
distcheck:
. .venv/bin/activate; twine check dist/*

distclean:
rm -rf dist

# Install, run, or update pre-commit hooks.

pre-commit: .git/hooks/pre-commit

.git/hooks/pre-commit: .pre-commit-config.yaml .venv/lib/python$(PYV)/site-packages/psycopg2.py
. .venv/bin/activate; pre-commit install --install-hooks
# Run the test suite.
test tests coverage: .coverage
.coverage: $(PYPACKAGE_NAME).egg-info tests/*.py
$(PYTEST) --cov=$(PYPACKAGE_NAME) $(PYTEST_ARGS)

check checks lint: .git/hooks/pre-commit
. .venv/bin/activate; pre-commit validate-config
. .venv/bin/activate; pre-commit validate-manifest
. .venv/bin/activate; pre-commit run --show-diff-on-failure --all-files
smoke: $(PYPACKAGE_NAME).egg-info tests/*.py
$(PYTEST) -m "smoke and not slow" $(PYTEST_ARGS)

# Install Stuart in a container image.
builder tester:
docker build -t stuart:$@ --target $@ .
# Run the linter (including unstaged changes).
lint: $(PRE_COMMIT_HOOKS)
$(PRE_COMMIT) run --show-diff-on-failure --all-files

container docker:
docker build -t stuart .
# Install the pre-commit hooks.
pre-commit: $(PRE_COMMIT_HOOKS)
.git/hooks/%: .pre-commit-config.yaml | setup
$(PRE_COMMIT) validate-config
$(PRE_COMMIT) validate-manifest
$(PRE_COMMIT) install --install-hooks --hook-type $*

prune:
docker system prune --all --volumes --force
# Set up the development environment.
setup: $(PYPACKAGE_NAME).egg-info
$(PYPACKAGE_NAME).egg-info: pyproject.toml src/*.py | bashbrew manifest-tool
. .venv/bin/activate; python -m pip install -e .[psycopg2cffi,dev,test]
echo "from psycopg2cffi import compat\ncompat.register()" \
> .venv/lib/python$(PYTHON_VERSION)/site-packages/psycopg2.py

bashbrew: .venv/bin/bashbrew

.venv/bin/bashbrew: .venv
.venv/bin/bashbrew: | venv
# cf. https://stackoverflow.com/a/40119933
$(eval TMP := $(shell mktemp -d))
git clone --depth=1 https://github.com/docker-library/bashbrew $(TMP)
Expand All @@ -97,42 +148,58 @@ bashbrew: .venv/bin/bashbrew
rm -rf $(TMP)

manifest-tool: .venv/bin/manifest-tool

.venv/bin/manifest-tool: .venv
.venv/bin/manifest-tool: | venv
$(eval TMP := $(shell mktemp -d))
git clone --depth=1 https://github.com/estesp/manifest-tool $(TMP)
cd $(TMP); make binary
cp $(TMP)/manifest-tool $@
rm -rf $(TMP)

# Install (or remove) build dependencies on Debian/Ubuntu. Note that
# these targets must be invoked by root. Also note that the
# purge-deps target can remove packages other that the ones listed
# here, so keep the confirmation prompts to avoid footguns.

DEBIAN_BUILD_DEPS = build-essential devscripts equivs postgresql
DEBIAN_INSTALL_TOOL = apt-get -o Debug::pkgProblemResolver=yes -y --no-install-recommends

build-deps: /etc/debian_version
ifneq ($(shell id -u), 0)
@echo You must be root to perform this action.
@exit 1
endif
sed -i '/deb-src/s/^# //' /etc/apt/sources.list
apt-get update
$(DEBIAN_INSTALL_TOOL) install $(DEBIAN_BUILD_DEPS)
mk-build-deps -i -r -t "$(DEBIAN_INSTALL_TOOL)" python3-psycopg2
mk-build-deps -i -r -t "$(DEBIAN_INSTALL_TOOL)" python3-psycopg2cffi
rm -f *.buildinfo *.changes

clean-deps: /etc/debian_version
ifneq ($(shell id -u), 0)
@echo You must be root to perform this action.
@exit 1
endif
apt-mark auto $(DEBIAN_BUILD_DEPS) psycopg2-build-deps python-psycopg2cffi-build-deps
apt-get autoremove
# Create the development environment.
venv: .venv
.venv:
$(PYTHON) -m venv $@
. .venv/bin/activate; python -m pip install -U pip-with-requires-python
. .venv/bin/activate; python -m pip install -U pip setuptools

# Remove build artifacts and reset the development environment.
clean:
rm -rf build .coverage dist stuart.egg-info .pytest_cache .venv*
rm -rf build .coverage dist *.egg-info .pytest_cache .venv* \
$(PRE_COMMIT_HOOKS)
find . -type d -name __pycache__ -print | xargs rm -rf

# Install development tools and build dependencies (requires local
# administrator rights).
build-deps:
$(if $(UNAME), \
$(if $(filter 0, $(or $(shell id -u))),, \
@echo You must be root to perform this action.; exit 1))
$(if $(filter Debian Ubuntu, $(DISTRO)), \
sed -i '/deb-src/s/^# //' /etc/apt/sources.list \
&& apt-get update \
&& (which jq > /dev/null || ($(APT_GET) install jq)) \
&& (which python3.12 > /dev/null \
|| (add-apt-repository -y ppa:deadsnakes/ppa \
&& $(APT_GET) install python3.12-full \
&& curl https://bootstrap.pypa.io/get-pip.py \
| python3.12 -)) \
&& (which mk-build-deps \
|| ($(APT_GET) install $(DEBIAN_BUILD_DEPS) \
&& mk-build-deps -i -r -t "$(APT_GET)" \
python3-psycopg2 \
&& mk-build-deps -i -r -t "$(APT_GET)" \
python3-psycopg2cffi \
&& rm -f *.buildinfo *.changes)))

# This could remove packages other that the ones listed, so keep any
# confirmation prompts (requires local administrator rights).
clean-deps:
$(if $(UNAME), \
$(if $(filter 0, $(or $(shell id -u))),, \
@echo You must be root to perform this action.; exit 1))
$(if $(filter Debian Ubuntu, $(DISTRO)), \
apt-mark auto \
$(DEBIAN_BUILD_DEPS) \
psycopg2-build-deps \
python-psycopg2cffi-build-deps \
&& apt-get autoremove)

0 comments on commit a917e48

Please sign in to comment.