branch: externals/plz commit 05f93b0b6bcbb865aa6f8d2254c360c47e82b353 Author: Adam Porter <a...@alphapapa.net> Commit: Adam Porter <a...@alphapapa.net>
Meta: Update Makefile, makem.sh --- Makefile | 32 +++++- makem.sh | 340 +++++++++++++++++++++++++++++++++++++++++++-------------------- 2 files changed, 264 insertions(+), 108 deletions(-) diff --git a/Makefile b/Makefile index 9be2568ad1..5000738761 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,29 @@ -# * Verbosity +# * Arguments + +# For consistency, we use only var=val options, not hyphen-prefixed options. + +# NOTE: I don't like duplicating the arguments here and in makem.sh, +# but I haven't been able to find a way to pass arguments which +# conflict with Make's own arguments through Make to the script. +# Using -- doesn't seem to do it. + +ifdef auto-install + AUTO_INSTALL = "--auto-install" +endif + +ifdef sandbox + SANDBOX = "--sandbox" +endif + +ifdef sandbox-dir + SANDBOX_DIR = "--sandbox-dir" "$(sandbox-dir)" +endif + +ifdef debug + DEBUG = "--debug" +endif + +# ** Verbosity # Since the "-v" in "make -v" gets intercepted by Make itself, we have # to use a variable. @@ -13,5 +38,8 @@ endif # * Rules +# TODO: Handle cases in which "test" or "tests" are called and a +# directory by that name exists, which can confuse Make. + %: - @./makem.sh $(VERBOSE) $(@) + @./makem.sh $(DEBUG) $(VERBOSE) $(SANDBOX) $(SANDBOX_DIR) $(AUTO_INSTALL) $(@) diff --git a/makem.sh b/makem.sh index 67a92dbd5b..40c49c5a2b 100755 --- a/makem.sh +++ b/makem.sh @@ -2,8 +2,32 @@ # * makem.sh --- Script to aid building and testing Emacs Lisp packages +# https://github.com/alphapapa/makem.sh + # * Commentary: +# makem.sh is a script helps to build, lint, and test Emacs Lisp +# packages. It aims to make linting and testing as simple as possible +# without requiring per-package configuration. + +# It works similarly to a Makefile in that "rules" are called to +# perform actions such as byte-compiling, linting, testing, etc. + +# Source and test files are discovered automatically from the +# project's Git repo, and package dependencies within them are parsed +# automatically. + +# Output is simple: by default, there is no output unless errors +# occur. With increasing verbosity levels, more detail gives positive +# feedback. Output is colored by default to make reading easy. + +# When desired, emacs-sandbox.sh can be used as a backend, which +# allows package dependencies to be installed automatically into a +# clean Emacs "sandbox" configuration without affecting the +# developer's personal configuration. This is especially helpful when +# upstream dependencies may have released new versions that differ +# from those installed in the developer's personal configuration. See +# <https://github.com/alphapapa/emacs-sandbox.sh>. # * License: @@ -20,63 +44,56 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. -# * Safety - -# NOTE: These are disabled by default in this template but should be -# enabled when feasible. Documentation is from the Bash man page. +# * Functions -# ** errexit +function usage { + cat <<EOF +$0 [OPTIONS] RULES... -# Exit immediately if a pipeline (which may consist of a single simple -# command), a list, or a compound command (see SHELL GRAMMAR above), -# exits with a non-zero status. The shell does not exit if the -# command that fails is part of the command list immediately following -# a while or until keyword, part of the test following the if or elif -# reserved words, part of any command executed in a && or || list -# except the command follow‐ ing the final && or ||, any command in a -# pipeline but the last, or if the command's return value is being -# inverted with !. If a compound command other than a subshell -# returns a non-zero status because a command failed while -e was -# being ignored, the shell does not exit. A trap on ERR, if set, is -# executed before the shell exits. This option applies to the shell -# environment and each subshell environment separately (see COMMAND -# EXECUTION ENVIRONMENT above), and may cause subshells to exit before -# executing all the commands in the subshell. +Rules: + all Run all lints and tests. + compile Byte-compile source files. -# If a compound command or shell function executes in a context where -# -e is being ignored, none of the commands executed within the -# compound command or function body will be affected by the -e -# setting, even if -e is set and a command returns a failure status. -# If a compound command or shell function sets -e while executing in a -# context where -e is ignored, that setting will not have any effect -# until the compound command or the command containing the function -# call completes. + lint Run all lints. + lint-checkdoc Run checkdoc. + lint-compile Byte-compile source files with warnings as errors. + lint-package Run package-lint. -# set -o errexit + test, tests Run all tests. + test-buttercup Run Buttercup tests. + test-ert Run ERT tests. -# ** nounset +Options: + -d, --debug Print debug info. + -h, --help I need somebody! + -v, --verbose Increase verbosity, up to -vv. + --debug-load-path Print load-path. -# Treat unset variables and parameters other than the special -# parameters "@" and "*" as an error when performing parameter -# expansion. If expansion is attempted on an unset variable or -# parameter, the shell prints an error message, and, if not -# interactive, exits with a non-zero status. + -f FILE, --file FILE Check FILE in addition to discovered files. -# NOTE: When this is not enabled, individual variables can be required -# to be set by using "${var:?}" parameter expansion syntax. + --no-color Disable color output. + -C, --no-compile Don't compile files automatically. -# set -o nounset +Sandbox options: + These require emacs-sandbox.sh to be on your PATH. Find it at + <https://github.com/alphapapa/emacs-sandbox.sh>. -# ** pipefail + -s, --sandbox Run Emacs with emacs-sandbox.sh in a temporary + directory (removing directory on exit). + -S, --sandbox-dir DIR Use DIR for the sandbox directory (leaving it + on exit). Implies -s. + --auto-install Automatically install package dependencies. + -i, --install PACKAGE Install PACKAGE before running rules. -# If set, the return value of a pipeline is the value of the last -# (rightmost) command to exit with a non-zero status, or zero if all -# commands in the pipeline exit successfully. This option is disabled -# by default. +Source files are automatically discovered from git, or may be +specified with options. -# set -o pipefail +Package dependencies are discovered from "Package-Requires" headers in +source files and from a Cask file. +EOF +} -# * Elisp +# ** Elisp # These functions return a path to an elisp file which can be loaded # by Emacs on the command line with -l or --load. @@ -140,26 +157,24 @@ EOF echo $file } -# * Functions - # ** Emacs function run_emacs { - debug "run_emacs: emacs -Q --batch --load=$package_initialize_file -L \"$load_path\" $@" + debug "run_emacs: $emacs_command -Q --batch --load=$package_initialize_file -L \"$load_path\" $@" if [[ $debug_load_path ]] then - debug $(emacs -Q --batch \ - --load=$package_initialize_file \ - -L "$load_path" \ - --eval "(message \"LOAD-PATH: %s\" load-path)" \ - 2>&1) + debug $($emacs_command -Q --batch \ + --load=$package_initialize_file \ + -L "$load_path" \ + --eval "(message \"LOAD-PATH: %s\" load-path)" \ + 2>&1) fi output_file=$(mktemp) - emacs -Q --batch \ - --load=$package_initialize_file \ - -L "$load_path" \ - "$@" \ + $emacs_command -Q --batch \ + --load=$package_initialize_file \ + -L "$load_path" \ + "$@" \ &>$output_file exit=$? @@ -195,17 +210,27 @@ function project-elisp-files { function project-source-files { # Echo list of Elisp files that are not tests. - project-elisp-files | egrep -v '^tests?/test-?' + project-elisp-files | egrep -v "$test_files_regexp" | feature-files } function project-test-files { # Echo list of Elisp test files. - project-elisp-files | egrep '^tests?/test-?' + project-elisp-files | egrep "$test_files_regexp" } function exclude-files { # Filter out paths (STDIN) which should be excluded by default. - egrep -v "(/\.cask/|-autoloads.el)" + egrep -v "(/\.cask/|-autoloads.el|.dir-locals)" +} + +function feature-files { + # Read paths on STDIN and echo ones that (provide 'a-feature). + while read path + do + debug "PATH: $path" + egrep "^\\(provide '" "$path" &>/dev/null \ + && echo "$path" + done } function load-files-args { @@ -224,6 +249,48 @@ function files_args { done } +function test-files-p { + # Return 0 if $project_test_files is non-empty. + [[ "${project_test_files[@]}" ]] +} + +function buttercup-tests-p { + # Return 0 if Buttercup tests are found. + test-files-p || die "No tests found." + debug "Checking for Buttercup tests..." + + grep "(require 'buttercup)" "${project_test_files[@]}" &>/dev/null +} + +function ert-tests-p { + # Return 0 if ERT tests are found. + test-files-p || die "No tests found." + debug "Checking for ERT tests..." + + # We check for this rather than "(require 'ert)", because ERT may + # already be loaded in Emacs and might not be loaded with + # "require" in a test file. + grep "(ert-deftest" "${project_test_files[@]}" &>/dev/null +} + +function dependencies { + # Echo list of package dependencies. + + # Search package headers. + egrep '^;; Package-Requires: ' $(project-source-files) $(project-test-files) \ + | egrep -o '\([^([:space:]][^)]*\)' \ + | egrep -o '^[^[:space:])]+' \ + | sed -r 's/\(//g' \ + | egrep -v '^emacs$' # Ignore Emacs version requirement. + + # Search Cask file. + if [[ -r Cask ]] + then + egrep '\(depends-on "[^"]+"' Cask \ + | sed -r -e 's/\(depends-on "([^"]+)".*/\1/g' + fi +} + # ** Utility function cleanup { @@ -308,40 +375,6 @@ function ts { date "+%Y-%m-%d %H:%M:%S" } -function usage { - cat <<EOF -$0 [OPTIONS] RULES... - -Rules: - all Run all lints and tests. - compile Byte-compile source files. - - lint Run all lints. - lint-checkdoc Run checkdoc. - lint-compile Byte-compile source files with warnings as errors. - lint-package Run package-lint. - - test, tests Run all tests. - test-buttercup Run Buttercup tests. - test-ert Run ERT tests. - -Options: - -d, --debug Print debug info. - -h, --help I need somebody! - -v, --verbose Increase verbosity, up to -vv. - - --debug-load-path Print load-path. - - -f FILE, --file FILE Check FILE in addition to discovered files. - - --no-color Disable color output. - -C, --no-compile Don't compile files automatically. - -Source files are automatically discovered from git, or may be -specified with options. -EOF -} - # * Rules # These functions are intended to be called as rules, like a Makefile. @@ -408,12 +441,14 @@ function lint-package { } function tests { - # Run tests. + verbose 1 "Running all tests..." + test-ert test-buttercup } function test-buttercup { + buttercup-tests-p || return 0 compile || die verbose 1 "Running Buttercup tests..." @@ -430,6 +465,7 @@ function test-buttercup { } function test-ert { + ert-tests-p || return 0 compile || die verbose 1 "Running ERT tests..." @@ -444,21 +480,37 @@ function test-ert { # * Defaults -# TODO: Disable color if not outputting to a terminal. -color=true +test_files_regexp='^(tests?|t)/' +emacs_command="emacs" errors=0 verbose=0 - compile=true -load_path="." -# TODO: Option to not byte-compile test files. -project_byte_compile_files=($(project-elisp-files)) -project_source_files=($(project-source-files)) -project_test_files=($(project-test-files)) +# MAYBE: Disable color if not outputting to a terminal. (OTOH, the +# colorized output is helpful in CI logs, and I don't know if, +# e.g. GitHub Actions logging pretends to be a terminal.) +color=true -package_initialize_file="$(elisp-package-initialize-file)" -temp_paths+=("$package_initialize_file") +# TODO: Using the current directory (i.e. a package's repo root directory) in +# load-path can cause weird errors in case of--you guessed it--stale .ELC files, +# the zombie problem that just won't die. It's incredible how many different ways +# this problem presents itself. In this latest example, an old .ELC file, for a +# .EL file that had since been renamed, was present on my local system, which meant +# that an example .EL file that hadn't been updated was able to "require" that .ELC +# file's feature without error. But on another system (in this case, trying to +# setup CI using GitHub Actions), the old .ELC was not present, so the example .EL +# file was not able to load the feature, which caused a byte-compilation error. + +# In this case, I will prevent such example files from being compiled. But in +# general, this can cause weird problems that are tedious to debug. I guess +# the best way to fix it would be to actually install the repo's code as a +# package into the sandbox, but doing that would require additional tooling, +# pulling in something like Quelpa or package-build--and if the default recipe +# weren't being used, the actual recipe would have to be fetched off MELPA or +# something, which seems like getting too smart for our own good. + +# TODO: Emit a warning if .ELC files that don't match any .EL files are detected. +load_path="." # ** Colors @@ -472,14 +524,32 @@ COLOR_purple='\e[0;35m' COLOR_cyan='\e[0;36m' COLOR_white='\e[0;37m' +# * Project files + +# MAYBE: Option to not byte-compile test files. (OTOH, byte-compiling reveals many +# errors that would otherwise go unnoticed, so it's worth it to fix the warnings.) +project_source_files=($(project-source-files)) +project_test_files=($(project-test-files)) +project_byte_compile_files=("${project_source_files[@]}" "${project_test_files[@]}") + +package_initialize_file="$(elisp-package-initialize-file)" +temp_paths+=("$package_initialize_file") + # * Args -args=$(getopt -n "$0" -o dhvf:C -l debug,debug-load-path,help,verbose,file:,no-color,no-compile -- "$@") || { usage; exit 1; } +args=$(getopt -n "$0" \ + -o dhi:sS:vf:C \ + -l auto-install,debug,debug-load-path,help,install:,verbose,file:,no-color,no-compile,sandbox,sandbox-dir: \ + -- "$@") \ + || { usage; exit 1; } eval set -- "$args" while true do case "$1" in + --auto-install) + auto_install=true + ;; -d|--debug) debug=true verbose=2 @@ -491,6 +561,18 @@ do usage exit ;; + -i|--install) + shift + sandbox_install_packages_args+=(--install "$1") + ;; + -s|--sandbox) + sandbox=true + ;; + -S|--sandbox-dir) + shift + sandbox=true + sandbox_dir="$1" + ;; -v|--verbose) ((verbose++)) ;; @@ -529,6 +611,52 @@ then exit 1 fi +if [[ $sandbox ]] +then + # Setup sandbox. + type emacs-sandbox.sh &>/dev/null || die "emacs-sandbox.sh not found." + + if ! [[ $sandbox_dir ]] + then + # No sandbox dir specified: make temp dir and remove it on exit. + sandbox_dir=$(mktemp -d) || die "Unable to make temp dir." + temp_paths+=("$sandbox_dir") + fi + + sandbox_basic_args=( + -d "$sandbox_dir" + ) + [[ $debug ]] && sandbox_basic_args+=(--debug) + + if [[ $auto_install ]] + then + # Add dependencies to package install list. + deps=($(dependencies)) + debug "Installing dependencies: ${deps[@]}" + + for package in "${deps[@]}" + do + sandbox_install_packages_args+=(--install $package) + done + fi + + if [[ ${sandbox_install_packages_args[@]} ]] + then + # Initialize the sandbox (installs packages once rather than for every rule). + emacs_command="emacs-sandbox.sh ${sandbox_basic_args[@]} ${sandbox_install_packages_args[@]} -- " + debug "Initializing sandbox..." + + run_emacs || die "Unable to initialize sandbox." + fi + + # After the sandbox is initialized and packages are installed, set the command + # to prevent the package lists from being refreshed on each invocation. + emacs_command="emacs-sandbox.sh ${sandbox_basic_args[@]} --no-refresh-packages -- " + + debug "Sandbox initialized." +fi + +# Run rules. for rule in "${rest[@]}" do if type "$rule" 2>/dev/null | grep "$rule is a function" &>/dev/null