From e23a026fe5cc2f7563743949bf08f98d72e017b9 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Sun, 9 Feb 2020 12:42:07 -0600 Subject: [PATCH] Meta: Update makem.sh, Makefile, and pre-push Use local .sandbox sandbox. (Assumes it is initialized.) --- Makefile | 16 ++- hooks/pre-push | 2 +- makem.sh | 380 ++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 320 insertions(+), 78 deletions(-) diff --git a/Makefile b/Makefile index 1ca3da4..ce9cdd8 100644 --- a/Makefile +++ b/Makefile @@ -19,11 +19,11 @@ ifdef install-linters endif ifdef sandbox - SANDBOX = "--sandbox" -endif - -ifdef sandbox-dir - SANDBOX_DIR = "--sandbox-dir" "$(sandbox-dir)" + ifeq ($(sandbox), t) + SANDBOX = --sandbox + else + SANDBOX = --sandbox $(sandbox) + endif endif ifdef debug @@ -49,4 +49,8 @@ endif # directory by that name exists, which can confuse Make. %: - @./makem.sh $(DEBUG) $(VERBOSE) $(SANDBOX) $(SANDBOX_DIR) $(INSTALL_DEPS) $(INSTALL_LINTERS) $(@) + @./makem.sh $(DEBUG) $(VERBOSE) $(SANDBOX) $(INSTALL_DEPS) $(INSTALL_LINTERS) $(@) + +.DEFAULT: init +init: + @./makem.sh $(DEBUG) $(VERBOSE) $(SANDBOX) $(INSTALL_DEPS) $(INSTALL_LINTERS) diff --git a/hooks/pre-push b/hooks/pre-push index 34976fa..45ef92a 100755 --- a/hooks/pre-push +++ b/hooks/pre-push @@ -14,4 +14,4 @@ read local_ref local_sha remote_ref remote_sha # cause remote code to be downloaded and executed (i.e. what would # happen by installing packages). It can be done manually when needed. -make test +make sandbox=.sandbox test diff --git a/makem.sh b/makem.sh index 9f3538c..cab8458 100755 --- a/makem.sh +++ b/makem.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # * makem.sh --- Script to aid building and testing Emacs Lisp packages @@ -59,17 +59,20 @@ Rules: lint Run all linters, ignoring unavailable ones. lint-checkdoc Run checkdoc. lint-compile Byte-compile source files with warnings as errors. - lint-indent Run indent-lint. + lint-declare Run check-declare. + lint-indent Lint indentation. lint-package Run package-lint. + lint-regexps Run relint. - test, tests Run all tests, ignoring missing test types. - test-buttercup Run Buttercup tests. - test-ert Run ERT tests. + test, tests Run all tests, ignoring missing test types. + test-buttercup Run Buttercup tests. + test-ert Run ERT tests. + test-ert-interactive Run ERT tests interactively. batch Run Emacs in batch mode, loading project source and test files automatically, with remaining args (after "--") passed to Emacs. interactive Run Emacs interactively, loading project source and test files - automatically. + automatically, with remaining args (after "--") passed to Emacs. Options: -d, --debug Print debug info. @@ -78,23 +81,33 @@ Options: --debug-load-path Print load-path from inside Emacs. - -f FILE, --file FILE Check FILE in addition to discovered files. + -E, --emacs PATH Run Emacs at PATH. + + -e, --exclude FILE Exclude FILE from linting and testing. + -f, --file FILE Check FILE in addition to discovered files. --no-color Disable color output. -C, --no-compile Don't compile files automatically. Sandbox options: - -s, --sandbox Run Emacs with an empty config in a temporary - directory (removing directory on exit). - -S, --sandbox-dir DIR Use DIR for the sandbox directory (leaving it - on exit). Implies -s. + -s, --sandbox [DIR] Run Emacs with an empty config in a sandbox DIR. + If DIR does not exist, make it. If DIR is not + specified, use a temporary sandbox directory and + delete it afterward, implying --install-deps and + --install-linters. --install-deps Automatically install package dependencies. --install-linters Automatically install linters. -i, --install PACKAGE Install PACKAGE before running rules. + An Emacs version-specific subdirectory is automatically made inside + the sandbox, allowing testing with multiple Emacs versions. When + specifying a sandbox directory, use options --install-deps and + --install-linters on first-run and omit them afterward to save time. + Source files are automatically discovered from git, or may be specified with options. Package dependencies are discovered from -"Package-Requires" headers in source files and from a Cask file. +"Package-Requires" headers in source files, from -pkg.el files, and +from a Cask file. EOF } @@ -142,11 +155,69 @@ function elisp-checkdoc-file { (when makem-checkdoc-errors-p (kill-emacs 1)))) +(setq checkdoc-spellcheck-documentation-flag t) (makem-checkdoc-files-and-exit) EOF echo $file } +function elisp-check-declare-file { + # Since check-declare doesn't have a batch function that exits + # non-zero when errors are found, we make one. + local file=$(mktemp) + + cat >$file <"$file" </dev/null | egrep "\.el$" | filter-files-exclude +function dirs-project { + # Echo list of directories to be used in load path. + files-project-feature | dirnames + files-project-test | dirnames } -function files-project-source { - # Echo list of Elisp files that are not tests. - files-project-elisp | egrep -v "$test_files_regexp" | filter-files-feature +function files-project-elisp { + # Echo list of Elisp files in project. + git ls-files 2>/dev/null \ + | egrep "\.el$" \ + | filter-files-exclude-default \ + | filter-files-exclude-args +} + +function files-project-feature { + # Echo list of Elisp files that are not tests and provide a feature. + files-project-elisp \ + | egrep -v "$test_files_regexp" \ + | filter-files-feature } function files-project-test { @@ -232,16 +315,42 @@ function files-project-test { files-project-elisp | egrep "$test_files_regexp" } -function filter-files-exclude { +function dirnames { + # Echo directory names for files on STDIN. + while read file + do + dirname "$file" + done +} + +function filter-files-exclude-default { # Filter out paths (STDIN) which should be excluded by default. egrep -v "(/\.cask/|-autoloads.el|.dir-locals)" } +function filter-files-exclude-args { + # Filter out paths (STDIN) which are excluded with --exclude. + if [[ ${files_exclude[@]} ]] + then + ( + # We use a subshell to set IFS temporarily so we can send + # the list of files to grep -F. This is ugly but more + # correct than replacing spaces with line breaks. Note + # that, for some reason, using IFS="\n" or IFS='\n' doesn't + # work, and a literal line break seems to be required. + IFS=" +" + grep -Fv "${files_exclude[*]}" + ) + else + cat + fi +} + function filter-files-feature { # 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 @@ -255,6 +364,14 @@ function args-load-files { done } +function args-load-path { + # Echo load-path arguments. + for path in $(dirs-project | sort -u) + do + printf -- '-L %q ' "$path" + done +} + function test-files-p { # Return 0 if $files_project_test is non-empty. [[ "${files_project_test[@]}" ]] @@ -283,7 +400,7 @@ function dependencies { # Echo list of package dependencies. # Search package headers. - egrep '^;; Package-Requires: ' $(files-project-source) $(files-project-test) \ + egrep '^;; Package-Requires: ' $(files-project-feature) $(files-project-test) \ | egrep -o '\([^([:space:]][^)]*\)' \ | egrep -o '^[^[:space:])]+' \ | sed -r 's/\(//g' \ @@ -295,6 +412,12 @@ function dependencies { egrep '\(depends-on "[^"]+"' Cask \ | sed -r -e 's/\(depends-on "([^"]+)".*/\1/g' fi + + # Search -pkg.el file. + if [[ $(git ls-files ./*-pkg.el 2>/dev/null) ]] + then + sed -nr 's/.*\(([-[:alnum:]]+)[[:blank:]]+"[.[:digit:]]+"\).*/\1/p' $(git ls-files ./*-pkg.el 2>/dev/null) + fi } # ** Sandbox @@ -308,7 +431,18 @@ function sandbox { if [[ $sandbox_dir ]] then # Directory given as argument: ensure it exists. - [[ -d $sandbox_dir ]] || die "Directory doesn't exist: $sandbox_dir" + if ! [[ -d $sandbox_dir ]] + then + debug "Making sandbox directory: $sandbox_dir" + mkdir -p "$sandbox_dir" || die "Unable to make sandbox dir." + fi + + # Add Emacs version-specific subdirectory, creating if necessary. + sandbox_dir="$sandbox_dir/$(emacs-version)" + if ! [[ -d $sandbox_dir ]] + then + mkdir "$sandbox_dir" || die "Unable to make sandbox subdir: $sandbox_dir" + fi else # Not given: make temp directory, and delete it on exit. local sandbox_dir=$(mktemp -d) || die "Unable to make sandbox dir." @@ -322,6 +456,7 @@ function sandbox { # Set sandbox args. This is a global variable used by the run_emacs function. args_sandbox=( + --title "makem.sh: $(basename $(pwd)) (sandbox: $sandbox_dir)" --eval "(setq user-emacs-directory (file-truename \"$sandbox_dir\"))" --eval "(setq user-init-file (file-truename \"$init_file\"))" "${args_load_init_file[@]}" @@ -339,6 +474,15 @@ function sandbox { done fi + # Add package-install arguments for linters. + if [[ $install_linters ]] + then + debug "Installing linters: package-lint relint" + + args_sandbox_package_install+=(--eval "(package-install 'package-lint)" + --eval "(package-install 'relint)") + fi + # *** Install packages into sandbox if [[ ${args_sandbox_package_install[@]} ]] @@ -485,6 +629,21 @@ function ts { date "+%Y-%m-%d %H:%M:%S" } +function emacs-version { + # Echo Emacs version number. + + # Don't use run_emacs function, which does more than we need. + "${emacs_command[@]}" -Q --batch --eval "(princ emacs-version)" \ + || die "Unable to get Emacs version." +} + +function rule-p { + # Return 0 if $1 is a rule. + [[ $1 =~ ^(lint-?|tests?)$ ]] \ + || [[ $1 =~ ^(batch|interactive)$ ]] \ + || [[ $(type -t "$2" 2>/dev/null) =~ function ]] +} + # * Rules # These functions are intended to be called as rules, like a Makefile. @@ -516,15 +675,16 @@ function batch { verbose 1 "Executing Emacs with arguments: ${args_batch[@]}" run_emacs \ - $(args-load-files "${files_project_source[@]}" "${files_project_test[@]}") \ - "${args_batch[@]}" + $(args-load-files "${files_project_feature[@]}" "${files_project_test[@]}") \ + "${args_batch_interactive[@]}" } function interactive { # Run Emacs interactively. Most useful with --sandbox and --install-deps. unset arg_batch run_emacs \ - $(args-load-files "${files_project_source[@]}" "${files_project_test[@]}") + $(args-load-files "${files_project_feature[@]}" "${files_project_test[@]}") \ + "${args_batch_interactive[@]}" arg_batch="--batch" } @@ -533,8 +693,10 @@ function lint { lint-checkdoc lint-compile + lint-declare lint-indent lint-package + lint-regexps } function lint-checkdoc { @@ -545,7 +707,7 @@ function lint-checkdoc { run_emacs \ --load="$checkdoc_file" \ - "${files_project_source[@]}" \ + "${files_project_feature[@]}" \ && success "Linting checkdoc finished without errors." \ || error "Linting checkdoc failed." } @@ -560,29 +722,32 @@ function lint-compile { unset compile_error_on_warn } +function lint-declare { + verbose 1 "Linting declarations..." + + local check_declare_file="$(elisp-check-declare-file)" + paths_temp+=("$check_declare_file") + + run_emacs \ + --load "$check_declare_file" \ + -f makem-check-declare-files-and-exit \ + "${files_project_feature[@]}" \ + && success "Linting declarations finished without errors." \ + || error "Linting declarations failed." +} + function lint-indent { - ensure-package-available indent-lint $1 || return $(echo-unset-p $1) - verbose 1 "Linting indentation..." - # FIXME: indent-lint outputs a summary line like: - # Diff finished (has differences). Fri Jan 17 10:30:34 2020 - # which is unnecessary for our use and clutters output. - # We load project source files as well, because they may contain # macros with (declare (indent)) rules which must be loaded to set - # indentation. However... - - # FIXME: This doesn't appear to actually work: macros that set - # indentation and are correctly indented in the source files are - # reported as having wrong indentation. Not sure if bug in - # indent-lint or here. + # indentation. run_emacs \ - --load indent-lint \ - $(args-load-files "${files_project_source[@]}" "${files_project_test[@]}") \ - --funcall indent-lint-batch \ - "${files_project_source[@]}" "${files_project_test[@]}" \ + --load "$(elisp-lint-indent-file)" \ + $(args-load-files "${files_project_feature[@]}" "${files_project_test[@]}") \ + --funcall makem-lint-indent-batch-and-exit \ + "${files_project_feature[@]}" "${files_project_test[@]}" \ && success "Linting indentation finished without errors." \ || error "Linting indentation failed." } @@ -595,11 +760,24 @@ function lint-package { run_emacs \ --load package-lint \ --funcall package-lint-batch-and-exit \ - "${files_project_source[@]}" \ + "${files_project_feature[@]}" \ && success "Linting package finished without errors." \ || error "Linting package failed." } +function lint-regexps { + ensure-package-available relint $1 || return $(echo-unset-p $1) + + verbose 1 "Linting regexps..." + + run_emacs \ + --load relint \ + --funcall relint-batch \ + "${files_project_source[@]}" \ + && success "Linting regexps finished without errors." \ + || error "Linting regexps failed." +} + function tests { verbose 1 "Running all tests..." @@ -607,6 +785,16 @@ function tests { test-buttercup } +function test-ert-interactive { + verbose 1 "Running ERT tests interactively..." + + unset arg_batch + run_emacs \ + $(args-load-files "${files_project_test[@]}") \ + --eval "(ert-run-tests-interactively t)" + arg_batch="--batch" +} + function test-buttercup { ensure-tests-available Buttercup $1 || return $(echo-unset-p $1) compile || die @@ -617,9 +805,8 @@ function test-buttercup { paths_temp+=("$buttercup_file") run_emacs \ - --load buttercup \ - --load "$buttercup_file" \ - -f buttercup-run-discover \ + $(args-load-files "${files_project_test[@]}") \ + -f buttercup-run \ && success "Buttercup tests finished without errors." \ || error "Buttercup tests failed." } @@ -672,7 +859,6 @@ color=true # 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 @@ -703,19 +889,11 @@ args_package_init=( elisp_org_package_archive="(add-to-list 'package-archives '(\"org\" . \"https://orgmode.org/elpa/\") t)" -# * 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.) -files_project_source=($(files-project-source)) -files_project_test=($(files-project-test)) -files_project_byte_compile=("${files_project_source[@]}" "${files_project_test[@]}") - # * Args args=$(getopt -n "$0" \ - -o dhi:sS:vf:CO \ - -l install-deps,install-linters,debug,debug-load-path,help,install:,verbose,file:,no-color,no-compile,no-org-repo,sandbox,sandbox-dir: \ + -o dhe:E:i:s:vf:CO \ + -l exclude:,emacs:,install-deps,install-linters,debug,debug-load-path,help,install:,verbose,file:,no-color,no-compile,no-org-repo,sandbox: \ -- "$@") \ || { usage; exit 1; } eval set -- "$args" @@ -727,12 +905,13 @@ do install_deps=true ;; --install-linters) - args_sandbox_package_install+=(--eval "(package-install 'indent-lint)" - --eval "(package-install 'package-lint)") + install_linters=true ;; -d|--debug) debug=true verbose=2 + args_debug=(--eval "(setq init-file-debug t)" + --eval "(setq debug-on-error t)") ;; --debug-load-path) debug_load_path=true @@ -741,21 +920,52 @@ do usage exit ;; + -E|--emacs) + shift + emacs_command=($1) + ;; -i|--install) shift args_sandbox_package_install+=(--eval "(package-install '$1)") ;; -s|--sandbox) sandbox=true - ;; - -S|--sandbox-dir) - shift - sandbox=true - sandbox_dir="$1" + # Check whether next argument is an option, rule, or a sandbox directory. + if [[ $2 ]] && ! [[ $2 =~ ^-- ]] \ + && ! rule-p "$2" + then + debug "Sandbox dir: $1" + shift + sandbox_dir="$1" + else + debug "No sandbox dir: installing dependencies." + install_deps=true + # HACK: Next argument is another option, so prepend blank arg to the + # argument list so it will be processed by next loop iteration. getopts + # doesn't allow options to have optional arguments, so we do this manually. + if [[ $2 =~ ^- ]] + then + # Next argument is an option: process it next. + new_args=("" "$@") + else + # Next argument is not an option: put it on the end. + new_arg="$2" + shift + shift + new_args=("" "$@" "$new_arg") + fi + debug "Setting new args: ${new_args[@]}" + set -- "${new_args[@]}" + fi ;; -v|--verbose) ((verbose++)) ;; + -e|--exclude) + shift + debug "Excluding file: $1" + files_exclude+=("$1") + ;; -f|--file) shift project_source_files+=("$1") @@ -792,27 +1002,54 @@ paths_temp+=("$package_initialize_file") trap cleanup EXIT INT TERM -if ! [[ ${files_project_source[@]} ]] +# Discover project files. +files_project_feature=($(files-project-feature)) +files_project_test=($(files-project-test)) +files_project_byte_compile=("${files_project_feature[@]}" "${files_project_test[@]}") + +debug "EXCLUDING FILES: ${files_exclude[@]}" +debug "FEATURE FILES: ${files_project_feature[@]}" +debug "TEST FILES: ${files_project_test[@]}" +debug "BYTE-COMPILE FILES: ${files_project_byte_compile[@]}" + +if ! [[ ${files_project_feature[@]} ]] then error "No files specified and not in a git repo." exit 1 fi +# Set load path. +args_load_paths=($(args-load-path)) +debug "LOAD PATH ARGS: ${args_load_paths[@]}" + +# If rules include linters and sandbox-dir is unspecified, install +# linters automatically. +if [[ $sandbox && ! $sandbox_dir ]] && [[ "${rest[@]}" =~ lint ]] +then + debug "Installing linters automatically." + install_linters=true +fi + # Initialize sandbox. [[ $sandbox ]] && sandbox # Run rules. for rule in "${rest[@]}" do - if [[ $batch ]] + if [[ $batch || $interactive ]] then - debug "Adding batch argument: $rule" - args_batch+=("$rule") + debug "Adding batch/interactive argument: $rule" + args_batch_interactive+=("$rule") elif [[ $rule = batch ]] then # Remaining arguments are passed to Emacs. batch=true + elif [[ $rule = interactive ]] + then + # Remaining arguments are passed to Emacs. + interactive=true + elif type -t "$rule" 2>/dev/null | grep function &>/dev/null then # Pass called-directly as $1 to indicate that the rule is @@ -828,8 +1065,9 @@ do fi done -# The batch rule. +# Batch/interactive rules. [[ $batch ]] && batch +[[ $interactive ]] && interactive if [[ $errors -gt 0 ]] then