# LOCAL MACHINE # This is a bunch of stuff that idk what even is this uname := $(shell uname) mkfile_dir := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST))))) gdatecmd := $(shell "command" -v gdate) ifdef gdatecmd gdate = gdate else gdate = date endif gdateok := $(shell 2>&1 $(gdate) --version | grep coreutils > /dev/null; echo $$?) ifneq ($(gdateok), 0) $(info **WARNING**: Missing coreutils "date" command. Please install coreutils.) # Needed for make dist endif gtarcmd := $(shell "command" -v gtar) ifdef gtarcmd gtar = gtar else gtar = tar endif gtarok := $(shell 2>&1 $(gtar) --version | grep 'GNU tar' > /dev/null; echo $$?) ifneq ($(gtarok), 0) $(info **WARNING**: Missing GNU "tar" command. Please install gnu-tar.) # Needed for make dist endif permstr = -perm +111 ifneq ($(uname), Darwin) permstr = -perm /111 endif # These sections set variables for make to track timestamps for the purposes of rebuilding files # PYTHON ENVIRONMENT PYTHONHASHSEED = 10 VIRTUAL_ENV ?= $(mkfile_dir)/.venv root_dir = $(shell pwd) # python = $(VIRTUAL_ENV)/bin/python3 python = $(shell find $(VIRTUAL_ENV)/bin -name "python3.12" -or -name "python3" 2>/dev/null | head -n 1) sphinx = $(python) -m sphinx pip = $(python) -m pip flake8 = $(python) -m flake8 sphinx-apidoc = $(python) -m sphinx.ext.apidoc # NSCLI version = $(shell $(python) -c "import sys; sys.path.insert(0, \"$(mkfile_dir)\"); import nscli; print(nscli.__version__)") requires.txt = nscli.egg-info/requires.txt #egginfo = nscli.egg-info config.db = nscli/data/config.db nscli_files = $(shell find nscli -type f \! -name \*.pyc) pkg_sources = $(nscli_files) $(manpages) $(completions) constraints.txt = constraints.txt # DOCUMENTATION email_src = docs/src/ReleaseEmail.rst email_html = docs/email/ReleaseEmail.html changelog = docs/src/Changelog.rst current_changelog = docs/src/CurrentChangelog.rst news_files = $(shell find docs/src/newsfragments -type f) regionlist = docs/src/RegionList.rst docs/src = $(shell find docs/src -type f -name \*.rst) webpage = docs/gh-pages manpages = docs/man/nscli.1.gz docs/man/nscli_settings.5.gz bash_completion = completions/bash/nscli zsh_completion = completions/zsh/_nscli completions = $(bash_completion) $(zsh_completion) # PYTHON PACKAGING tarball = dist/nscli-$(version).tar.gz wheel = dist/nscli-$(version)-py3-none-any.whl # DEBIAN PACKAGING # The .deb installer files encode a lot of information in the filename. Here we declare the filenames we wish to create, # which makes for easy targets. The recipe to make the .deb files parses relevant information from the desired filename # (using the debparse_* macros defined below) and passes it to the build environment. To support a new .deb release, # just declare the filename below and include it in $(debian_releases) for convenience. There needs to be a # corresponding docker image available on the hub; for example, debian:10 or ubuntu:19.04. ubuntu2204 = dist/debian/nscli_$(version)-1ubuntu1~22.04_amd64.deb ubuntu2404 = dist/debian/nscli_$(version)-1ubuntu1~24.04_amd64.deb debian_releases = $(ubuntu2204) $(ubuntu2404) debian_control_files = $(shell find debian -type f) debparse_dist_id = $(shell echo $(1) | cut -d '-' -f 2 | cut -d '~' -f 1 | tr -d '[:digit:]') debparse_dist_ver = $(shell echo $(1) | cut -d '-' -f 2 | cut -d '~' -f 2 | cut -d '_' -f 1) debparse_image_name = nscli_builder_$(call debparse_dist_id,$1)_$(call debparse_dist_ver,$1):$(version) debparse_codename = $(shell docker run --rm $(call debparse_image_name,$1) grep -m 1 CODENAME /etc/os-release | cut -d '=' -f 2) .PHONY: all all: help .PHONY: clean clean: docclean ## Delete temporary files -rm -rf dist build .eggs nscli.egg-info .tox .coverage htmlcov coverage.xml .config share version -rm -f $(config.db) -find nscli -name __pycache__ -exec rm -rf "{}" \; -find nscli -name '*.pyc' -exec rm -f "{}" \; .PHONY: distclean distclean: clean ## Delete anything that's not part of the repo git reset HEAD --hard git clean -fxd .PHONY: imageclean imageclean: docker rmi $(docker images --filter=reference='*/nscli:*' -q) 2> /dev/null || echo "no nscli images found" .PHONY: test test: $(config.db) $(manpages) $(python) $(completions) ## Run tests # Ensure necessary test credentials are loaded $(python) -m nscli.tests.tools # Run test suites $(python) -m tox # make the tar.gz and whl .PHONY: dist dist: $(config.db) $(pkg_sources) $(manpages) $(python) | version_check $(python) -m build .PHONY: debian debian: $(debian_releases) ## Create new Debian and Ubuntu packages .PHONY: debian22 debian22: $(ubuntu2204) .PHONY: debian24 debian24: $(ubuntu2404) .PHONY: webpage webpage: $(webpage) ## Regenerate the webpage files # something happened with editable installs and the location returned from importlib.metadata.distribution("nscli") # we create a symlink to trick python that has nscli installed as an editable package where to find the files .PHONY: site_link site_link: ln -s $(root_dir)/nscli $(shell $(python) -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])') || echo "Link already exists" .PHONY: devenv devenv: ## Set up your nscli dev environment. Creates a virtual environment. # ensure VENV is not active if [[ ! -z $$VIRTUAL_ENV ]]; then echo ; echo "!! ERROR !! Deactivate VENV before running 'make devenv'"; echo; exit 1; fi # install dev packages from lockfile and don't update lockfile -pdm venv remove in-project -y pdm sync -G dev make $(config.db) .PHONY: build_env build_env: ## Set up your nscli dev environment. Creates a virtual environment. # ensure VENV is not active if [[ ! -z $$VIRTUAL_ENV ]]; then echo ; echo "!! ERROR !! Deactivate VENV before running 'make build_env'"; echo; exit 1; fi -pdm venv remove in-project -y # install build packages from lockfile and don't update lockfile pdm sync -G build make $(config.db) .PHONY: release_env release_env: # Set up your nscli dev environment. Creates a virtual environment. # ensure VENV is not active if [[ ! -z $$VIRTUAL_ENV ]]; then echo ; echo "!! ERROR !! Deactivate VENV before running 'make release_env'"; echo; exit 1; fi -pdm venv remove in-project -y # install prod packages from lockfile and don't update lockfile pdm sync --prod make $(config.db) .PHONY: lint lint: $(python) version_check ## Check code style # Check for files with executable bits set. This should only be nscli/shared/2048.sh and nscli/__main__.py $(eval execs:=$(shell find nscli -type f $(permstr))) test "$(execs)" = "nscli/shared/2048.sh nscli/__main__.py" -o "nscli/__main__.py nscli/shared/2048.sh" # All directories and Python files in NSCLI must be lowercase and must not include spaces or dashes. ! find nscli -type f -name '*.py' -o -type d | grep -e '[A-Z ]' # All Python packages under nscli must include a __init__.py file. If a directory contains Python code but no init, # the next line will error out. test -z `find nscli -type d '!' -exec test -e "{}/__init__.py" ';' -exec find "{}" -name '*.py' ';'` $(python) scripts/plugin_checker.py $(flake8) --config=$(mkfile_dir)/pyproject.toml .PHONY: webpage_upload webpage_upload: $(webpage) ## Upload the nscli documentation webpage to the gh-pages branch in GitHub # Verify the gh_pages branch exists. This branch should be pulled down, not created new git rev-parse --verify gh-pages || (echo "gh-pages branch not found. Try 'git fetch --all && git checkout gh-pages'" && exit 1) $(eval branch := $(shell git rev-parse --abbrev-ref HEAD)) git checkout gh-pages git pull git rm -rf . # un-stage all files git add docs/gh-pages/* # stage only the output files we need git mv docs/gh-pages/* . # move output files to root of repo touch .nojekyll && git add .nojekyll # This file tells github to serve up the site as-is git commit --all -m "rebuild docs" git push git checkout $(branch) -$(python) -c 'import webbrowser; webbrowser.open("https://gh-pages.pvceng.rax.io/RSS-Engineering/nsi-nscli/")' .PHONY: email email: $(email_html) $(python) ## Send the release-day email $(python) scripts/send_release_email.py -t .PHONY: upload upload: ## Upload a new distribution to artifactory from the dist/ folder. pdm publish --no-build --repository https://artifacts.rackspace.net/artifactory/api/pypi/nscli-pypi-local .PHONY: version_check version_check: $(python) @echo Validating version number... @$(python) scripts/versioning.py .PHONY: docclean docclean: # Delete cached documentation to avoid a lot of bugs in Sphinx. Especially required for sphinx_selective_exclude -find . -name .doctrees -exec rm -rf "{}" \; .PHONY: help help: # Help function shamelessly stolen from the Rackspace Engineering Handbook @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' .PHONY: buildcontainer buildcontainer: # Build the nscli testing environment locally. Best left to Jenkins. docker build --progress=plain --pull -t nscli_base:latest . test -f docker/credentials.json echo 'FROM nscli_base:latest' | docker build --progress=plain -t nscli_loaded:latest . -f - # this creates a requirements.txt with "prod" packages from the .egg-info, the flag to use artifactory, and a forced install of importlib_metadata requirements.txt: $(requires.txt) @printf "%s\n" '-i https://artifacts.rackspace.net/artifactory/api/pypi/pypi/simple/' > requirements.txt @cat $(requires.txt) | sed -e '/^$$/,$$d' >> requirements.txt # TODO: This next line is a gross hack @printf "%s\n" importlib_metadata >> requirements.txt $(constraints.txt): pdm.lock scripts/generate_constraints.py $(python) scripts/generate_constraints.py > constraints.txt version: nscli/__init__.py @printf "$(version)\n" > version && chmod 644 version %.deb: requirements.txt $(pkg_sources) $(manpages) $(debian_control_files) # $@ will be the target name (the deb file) @mkdir -p "$(dir $@)" # we use docker so we can build the .deb package in the exact Ubuntu version target environment docker build --no-cache --platform linux/amd64 --progress=plain --pull -t $(call debparse_image_name,$@) \ --build-arg VERSION=$(version) \ --build-arg DIST_VERSION=$(call debparse_dist_ver,$@) \ --build-arg DIST_ID=$(call debparse_dist_id,$@) \ -f debian/Dockerfile . # change dir to /dpkg and create a new archive named for the deb target (notdir strips the directory path leaving just the filename) # pipe this into a verbose extraction of the archive into the directory specified by $(dir $@) (i.e. the dir named after the deb target) # --rm deletes the container after use docker run --platform linux/amd64 --rm $(call debparse_image_name,$@) tar -C /dpkg -c "$(notdir $@)" | tar -C "$(dir $@)" -xv .PHONY: debian_upload debian_upload: $(debian_releases:.deb=.deb.upload) ## Uploads the .deb release files to artifactory %.deb.upload: %.deb $(eval FILE=$*.deb) @if test -z "$(API_TOKEN)"; then \ echo "The API_TOKEN environment variable for artifactory is not exported."; \ exit 1; \ fi @if curl -fsI "https://artifacts.rackspace.net/artifactory/nscli-debian-local/pool/$(notdir $(FILE))" > /dev/null; then \ echo $(notdir $(FILE)) already exists on artifactory.; \ return 0; \ fi @echo Uploading $(FILE) @curl -i -H "Authorization: Bearer $(API_TOKEN)" -XPUT -T $(FILE) \ -H "X-Checksum-Sha1: $(shell shasum -a 1 $(FILE) | awk '{print $$1}')" -H "X-Checksum-Md5: $(shell md5sum $(FILE) | awk '{print $$1}')" \ "https://artifacts.rackspace.net/artifactory/nscli-debian-local/pool/$(notdir $(FILE));deb.distribution=$(call debparse_codename,$(FILE));deb.component=main;deb.architecture=amd64" && echo $(python): make build_env $(webpage): $(regionlist) $(docs/src) $(python) $(constraints.txt) PYTHONHASHSEED=$(PYTHONHASHSEED) $(sphinx) -b html docs/src docs/gh-pages -D version="$(version)" -D release="$(version)" cp constraints.txt docs/gh-pages/ # NOTE: The $(webpage) target depends on $(docs/src), which is a shell command that expands # to all .rst files in docs/src/ (including Changelog.rst). Because Changelog.rst is a # prerequisite, Make looks for a rule to build it and finds the $(changelog) target below. # Make is a timestamp-based build system that works backwards through the dependency chain, # rebuilding anything that's out of date before building what you asked for. It checks if # Changelog.rst is older than any newsfragments (comparing file modification times) and if # newsfragments are newer, Make automatically runs towncrier to rebuild the changelog before # building the webpage. This is why 'make webpage' automatically consumes newsfragments # without explicitly calling 'make changelog'. .PHONY: changelog changelog: $(changelog) ## Updates the changelog from the news files $(changelog): $(news_files) $(python) -m towncrier --yes $(current_changelog): $(changelog) head -n $$(($$(grep -n '=======' docs/src/Changelog.rst | head -n 2 | tail -n 1 | cut -d ':' -f 1)-2)) $(changelog) > $(current_changelog) $(email_html): $(email_src) $(current_changelog) $(python) | docclean $(sphinx) -t email -b html docs/src docs/email -D version="$(version)" -D release="$(version)" $(regionlist): $(config.db) $(python) $(python) scripts/print_regions.py > $(regionlist) $(config.db): $(python) site_link $(python) -c 'from nscli.config_db import DataBase; DataBase("nscli/data/config.db")'; .PHONY: manpage manpage: $(manpages) ## Rebuild the man pages $(manpages): docs/src $(regionlist) $(python) | docclean PYTHONHASHSEED=$(PYTHONHASHSEED) $(sphinx) -b man docs/src docs/man -D version="$(version)" -D release="$(version)" -rm -rf docs/man/*.gz gzip --no-name -9 docs/man/* .PHONY: completions completions: $(completions) ## Rebuild the shell completion files $(zsh_completion): $(nscli_files) $(python) @mkdir -p completions/zsh $(python) -m nscli settings --zsh > $(zsh_completion) $(bash_completion): $(nscli_files) $(python) @mkdir -p completions/bash $(python) -m nscli settings --bash > $(bash_completion) needs_review: ## Generate a list of files that need review per NSCLI's SDLC policy. @-rm -f needs_review @$(eval all_files := $(shell git ls-tree --name-only -r HEAD nscli)) @echo "Evaluating each file's commit history, please wait..." @for f in $(all_files); do \ found=$$(git log --since='1 year ago' --pretty=format:"%ct %s" -- "$$f" | grep 'SDLC Review Passed'); \ if [ -z "$$found" ]; then \ git log --pretty=format:"%ct $$f%n" -n 1 -- "$$f" >> needs_review; \ fi; \ done @sort needs_review > needs_review_tmp @mv needs_review_tmp needs_review