Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️ Extract make command groups #805

Merged
merged 11 commits into from
Nov 14, 2022
1 change: 1 addition & 0 deletions .makefile
4 changes: 2 additions & 2 deletions {{cookiecutter.project_slug}}/.github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,8 @@ jobs:
- name: Get container image metadata
id: container-image-metadata
run: |
CI_TAGGED_IMG=$(echo -n $(make get-make-var-IMG get-project-version-number) | tr ' ' '-')
CANONICAL_TAGGED_IMGS=$(echo -n $(make get-make-var-IMG get-make-var-LATEST_IMG) | tr ' ' ',')
CI_TAGGED_IMG=$(echo -n $(make get-docker-img-strong-version-tag get-project-version-number) | tr ' ' '-')
CANONICAL_TAGGED_IMGS=$(echo -n $(make get-docker-img-strong-version-tag get-docker-img-latest-tag) | tr ' ' ',')
echo "::set-output name=tagged_images::${CI_TAGGED_IMG},${CANONICAL_TAGGED_IMGS}"

- name: Build and push container image
Expand Down
105 changes: 105 additions & 0 deletions {{cookiecutter.project_slug}}/.makefile/docker.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#################################################################################
# GLOBALS #
#################################################################################
REGISTRY_NAMESPACE = {{cookiecutter.container_registry_namespace}}
DOCKER_REPOSITORY = $(REGISTRY_NAMESPACE)/$(PROJECT_NAME)

IMG := $(DOCKER_REPOSITORY):$(TAG)
LATEST_IMG := $(DOCKER_REPOSITORY):latest

.PHONY: get-docker-img-strong-version-tag
get-docker-img-strong-version-tag:
@echo $(IMG)

.PHONY: get-docker-img-latest-tag
get-docker-img-latest-tag:
@echo $(LATEST_IMG)

#################################################################################
# HELPER TARGETS #
#################################################################################
# Check that given variables are set and all have non-empty values,
# die with an error otherwise.
#
# Params:
# 1. Variable name(s) to test.
# 2. (optional) Error message to print.
check_defined = \
$(strip $(foreach 1,$1, \
$(call __check_defined,$1,$(strip $(value 2)))))
__check_defined = \
$(if $(value $1),, \
$(error Undefined $1$(if $2, ($2))))

.PHONY: _validate-req-env-vars
_validate-req-env-vars:
$(call check_defined, REQ_ENV_VARS, Error: Required list of env vars to validate as defined not set!)
$(foreach REQ_ENV_VAR,$(REQ_ENV_VARS),$(call check_defined, $(REQ_ENV_VAR), Error: Required env var not set!))

.PHONY: pull-container
pull-container:
## Pull {{cookiecutter.project_slug}} container
pull-container:
- docker pull $(LATEST_IMG)

.PHONY: push-container
push-container:
## Push {{cookiecutter.project_slug}} container
push-container: build-container
docker push $(IMG)
docker push $(LATEST_IMG)

.PHONY: build-container
build-container: export DOCKER_BUILDKIT=1
## Build {{cookiecutter.project_slug}} container
build-container:
docker build --tag $(IMG) .
@echo Built $(IMG)
@if ! [ "$(TAG)" = latest ]; then \
docker tag $(IMG) $(LATEST_IMG) && \
echo Built $(LATEST_IMG); \
fi

.PHONY: stop-container
## Stop container forcefully (i.e., when keyboard interrupts are disabled)
stop-container:
- docker stop $(PROJECT_NAME)
- docker rm $(PROJECT_NAME)

#################################################################################
# COMMANDS #
#################################################################################
#{% set PROJECT_TYPE = 'jupyter' if cookiecutter.jupyter_notebook_project == 'yes' else 'project' %}
.PHONY: deploy-{{ PROJECT_TYPE }}-docker-container-local
## Deploy locally-built dockerized {{ PROJECT_TYPE }} environment with preloaded dependencies
deploy-{{ PROJECT_TYPE }}-docker-container-local: build-container deploy-container

.PHONY: deploy-{{ PROJECT_TYPE }}-docker-container
## Deploy downloaded dockerized {{ PROJECT_TYPE }} environment with preloaded dependencies
deploy-{{ PROJECT_TYPE }}-docker-container: pull-container deploy-container

.PHONY: deploy-container
deploy-container: BIND_MOUNT_APPLICATION_DIR_ON_CONTAINER ?= false
deploy-container: IS_INTERACTIVE_SESSION ?= false
{% if PROJECT_TYPE == 'jupyter' -%} deploy-container: PORT?=8888 {%- endif %}
# If you want to validate that a set of pre-defined env vars are specified
# prior to container launch, list them in the REQ_ENV_VARS array
deploy-container: REQ_ENV_VARS := BIND_MOUNT_APPLICATION_DIR_ON_CONTAINER IS_INTERACTIVE_SESSION {% if PROJECT_TYPE == 'jupyter' -%} PORT {%- endif %}
deploy-container: _validate-req-env-vars stop-container
{%- if PROJECT_TYPE == 'jupyter' %}
@echo "Launching Jupyter Notebook on port $(PORT)" {% else %}
@echo "Launching $(PROJECT_NAME) container"
{%- endif %}
@if [ "$(IS_INTERACTIVE_SESSION)" != false ]; then \
export INTERACTIVE_SESSION_ARGS="--interactive --tty --entrypoint bash"; \
fi && \
if [ "$(BIND_MOUNT_APPLICATION_DIR_ON_CONTAINER)" != false ]; then \
export BIND_MOUNT_APPLICATION_DIR_ON_CONTAINER_ARGS="--mount type=bind,source=$$(pwd)/$(SRC_DIR),target=/app/$(SRC_DIR)"; \
fi && \
docker run \
--name $(PROJECT_NAME) {% if PROJECT_TYPE == 'jupyter'-%} --publish $(PORT):8888 {%- endif %} \
$${INTERACTIVE_SESSION_ARGS} \
$${BIND_MOUNT_APPLICATION_DIR_ON_CONTAINER_ARGS} \
-w /app/$(SRC_DIR) \
$(LATEST_IMG)
$(MAKE) stop-container
23 changes: 23 additions & 0 deletions {{cookiecutter.project_slug}}/.makefile/docs.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#################################################################################
# COMMANDS #
#################################################################################
.PHONY: docs-%
## Build documentation in the format specified after `-`
## (e.g.,
## `make docs-html` builds the docs in HTML format,
## `make docs-confluence` builds and publishes the docs on confluence (see `docs/source/conf.py` for details),
## `make docs-clean` cleans the docs build directory)
docs-%:
$(MAKE) $* -C docs

.PHONY: test-docs
## Test documentation format/syntax
test-docs:
poetry run tox -e docs

#{%- if cookiecutter.adr_documentation_support == 'yes' %}
.PHONY: docs-adl-preview
## Launch live preview of ADR documentation
docs-adl-preview:
poetry run log4brains preview
#{%- endif %}
61 changes: 61 additions & 0 deletions {{cookiecutter.project_slug}}/.makefile/help.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#################################################################################
# Self Documenting Commands #
#################################################################################

# Inspired by <http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html>
# sed script explained:
# /^##/:
# * save line in hold space
# * purge line
# * Loop:
# * append newline + line to hold space
# * go to next line
# * if line starts with doc comment, strip comment character off and loop
# * remove target prerequisites
# * append hold space (+ newline) to line
# * replace newline plus comments by `---`
# * print line
# Separate expressions are necessary because labels cannot be delimited by
# semicolon; see <http://stackoverflow.com/a/11799865/1968>
.PHONY: help
help:
ifdef DESCRIPTION
@echo "$$(tput bold)Description:$$(tput sgr0)" && echo "$$DESCRIPTION" && echo
endif
@echo "$$(tput bold)Available rules:$$(tput sgr0)"
@echo
@sed -n -e "/^## / { \
h; \
s/.*//; \
:doc" \
-e "H; \
n; \
s/^## //; \
t doc" \
-e "s/:.*//; \
G; \
s/\\n## /---/; \
s/\\n/ /g; \
p; \
}" ${MAKEFILE_LIST} \
| LC_ALL='C' sort --ignore-case \
| awk -F '---' \
-v ncol=$$(tput cols) \
-v indent=19 \
-v col_on="$$(tput setaf 6)" \
-v col_off="$$(tput sgr0)" \
'{ \
printf "%s%*s%s ", col_on, -indent, $$1, col_off; \
n = split($$2, words, " "); \
line_length = ncol - indent; \
for (i = 1; i <= n; i++) { \
line_length -= length(words[i]) + 1; \
if (line_length <= 0) { \
line_length = ncol - indent - length(words[i]) - 1; \
printf "\n%*s ", -indent, " "; \
} \
printf "%s ", words[i]; \
} \
printf "\n"; \
}' \
| more $(shell test $(shell uname) = Darwin && echo '--no-init --raw-control-chars')
103 changes: 103 additions & 0 deletions {{cookiecutter.project_slug}}/.makefile/project_orchestration.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#################################################################################
# HELPER TARGETS #
#################################################################################
.PHONY: _validate-poetry-installation
_validate-poetry-installation:
ifeq ($(shell command -v poetry),)
@echo "poetry could not be found!"
@echo "Please install poetry!"
@echo "Ex.: 'curl -sSL \
https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python - \
&& source $$HOME/.local/env'"
@echo "see:"
@echo "- https://python-poetry.org/docs/#installation"
@echo "Note: 'pyenv' recommended for Python version management"
@echo "see:"
@echo "- https://github.com/pyenv/pyenv"
@echo "- https://python-poetry.org/docs/managing-environments/"
false
else
@echo "Using $(shell poetry --version) in $(shell which poetry)"
endif

.PHONY: update-dependencies
## Update and install Python dependencies,
## updating packages in `poetry.lock` with any newer versions
## that adhere to `pyproject.toml` version range constraints
update-dependencies:
poetry update --lock
ifneq (${CI}, true)
$(MAKE) install-dependencies
endif

.PHONY: install-dependencies
## Install Python dependencies specified in `poetry.lock`
install-dependencies:
poetry install --no-interaction --no-root --with docs -vv
#{%- if cookiecutter.adr_documentation_support == 'yes' %}
# Install node ADR management library
poetry run nodeenv --python-virtualenv --jobs=$(NUM_PROCS)
poetry run npm install -g --no-package-lock --no-save log4brains
#{%- endif %}

.PHONY: install-project
## Install {} source code (in editable mode)
install-project:
poetry install --no-interaction -vv
$(MAKE) clean

.PHONY: clean
## Delete all compiled Python files
clean:
find . -type f -name "*.py[co]" -delete
find . -type d -name "__pycache__" -delete
# Clean up files in source directories that may have been generated from C extension compilation
find . -type f -name "*.so" -delete -maxdepth 2
find . -type f -name "*.pyd" -delete -maxdepth 2

.PHONY: get-project-version-number
## Echo project's canonical version number
get-project-version-number:
@poetry version --short

#################################################################################
# COMMANDS #
#################################################################################
.PHONY: provision-environment
## Set up a Python virtual environment with installed project dependencies
provision-environment: _validate-poetry-installation install-dependencies install-project

.PHONY: install-pre-commit-hooks
## Install git pre-commit hooks locally
install-pre-commit-hooks:
poetry run pre-commit install

#{%- if cookiecutter.project_type == 'package' %}
.PHONY: package
## Build project package(s)
package:
tox -e package
#{%- endif %}

# Note: The new version should ideally be a valid semver string or a valid bump rule:
# "patch", "minor", "major", "prepatch", "preminor", "premajor", "prerelease".
.PHONY: bump-commit-and-push-project-version-number-%
## *ATOMICALLY*:
## 1.) Bump the version of the project
## 2.) Write the new version back to `pyproject.toml` (assuming a valid bump rule is provided)
## 3.) Commit the version bump to VCS
## 4.) Push the commit to the remote repository
## (e.g.,
## `bump-commit-and-push-project-version-number-patch`,
## `bump-commit-and-push-project-version-number-minor`,
## etc.)
bump-commit-and-push-project-version-number-%: VERSION_NUM_FILE:=pyproject.toml
bump-commit-and-push-project-version-number-%:
# shell out to ensure next line gets updated version number;
# directly running `poetry version $*` will cause next line to NOT pick up the version bump
@echo "$(shell poetry version $*)"
@export NEW_VER_NUM=$(shell $(MAKE) get-project-version-number) && \
export COMMIT_MSG=":bookmark: Bump version number to \`$${NEW_VER_NUM}\`" && \
git commit $(VERSION_NUM_FILE) -m "$${COMMIT_MSG}" && \
git push \
|| git checkout HEAD -- $(VERSION_NUM_FILE) # Rollback `VERSION_NUM_FILE` file on failure
84 changes: 84 additions & 0 deletions {{cookiecutter.project_slug}}/.makefile/test.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#################################################################################
# HELPER TARGETS #
#################################################################################
.PHONY: generate-requirements
## Generate project requirements files from `pyproject.toml`
generate-requirements:
poetry export --format requirements.txt --without-hashes --output requirements.txt # subset
poetry export --with dev --format requirements.txt --without-hashes --output requirements-dev.txt # superset w/o docs
poetry export --with dev,docs --format requirements.txt --without-hashes --output requirements-all.txt # superset

.PHONY: clean-requirements
## Clean generated project requirements files
clean-requirements:
find . -type f -name "requirements*.txt" -delete -maxdepth 1

.PHONY: tox-%
## Run specified tox testenvs
tox-%: generate-requirements
poetry run tox -e "$*" -- $(POSARGS)
$(MAKE) clean-requirements

#################################################################################
# COMMANDS #
#################################################################################
.PHONY: test
ifeq (${CI}, true)
test: export TOX_PARALLEL_NO_SPINNER=1
endif
## Run pre-defined test suite via tox
test: generate-requirements
poetry run tox --parallel
$(MAKE) clean-requirements

.PHONY: test-%
## Run Python version-specific tests (e.g., `make test-py39`)
test-%:
$(MAKE) tox-$*,coverage

#{%- if cookiecutter.jupyter_notebook_project != 'yes' %}
.PHONY: performance-benchmarks
## Run performance benchmark tests
performance-benchmarks:
$(MAKE) performance-benchmarks-"{pure_python,c_library}"

.PHONY: performance-benchmarks-%
# Run library-specific (viz. Python or C) performance benchmark tests
performance-benchmarks-%:
$(MAKE) tox-"py3{8,9}-benchmark-$*"

# Mutation testing modifies the code in small ways that should produce incorrect semantics
# If a test suite is sufficiently strong, this "mutated" code should caught by the suite,
# thus causing tests to fail.
.PHONY: test-mutations
## Run mutation testing to validate test suite robustness
test-mutations:
$(MAKE) tox-mutmut
#{%- endif %}

.PHONY: lint
## Run full static analysis suite for local development
lint:
$(MAKE) scan-dependencies
$(MAKE) pre-commit

.PHONY: scan-dependencies
## Scan dependencies for security vulnerabilities
scan-dependencies:
$(MAKE) tox-security

.PHONY: pre-commit
## Lint using *ALL* pre-commit hooks
## (see `.pre-commit-config.yaml`)
pre-commit:
# Note: Running through `tox` since some hooks rely on finding their executables
# in the `.tox/precommit/bin` directory and to provide an extra layer of isolation
# for reproducibility.
$(MAKE) tox-precommit POSARGS=$(PRECOMMIT_HOOK_ID)

.PHONY: pre-commit-%
## Lint using a *SINGLE* specific pre-commit hook (e.g., `make pre-commit-mypy`)
## (see `.pre-commit-config.yaml`)
pre-commit-%: export SKIP= # Reset `SKIP` env var to force single hooks to always run
pre-commit-%:
$(MAKE) pre-commit PRECOMMIT_HOOK_ID=$*
Loading