diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1bd7f12f5..839f94494 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -58,7 +58,8 @@ jobs:
- name: Byte compilation
run: emacs --eval "(setq byte-compile-error-on-warn (>= emacs-major-version 26))" -L . --batch -f batch-byte-compile ./*.el
- name: Tests
- run: nix shell ${{ matrix.ledger_version || 'nixpkgs#ledger' }} --print-build-logs -c make -C test
+ run: nix shell 'nixpkgs#getopt' ${{ matrix.ledger_version || 'nixpkgs#ledger' }} --print-build-logs -c ./makem.sh test -vv
# This is currently for information only, since a lot of docstrings need fixing up
- name: Checkdoc
- run: make -C test checkdoc || true
+ run: make lint-checkdoc || true
+ # TODO: Run other makem.sh lints
diff --git a/.gitignore b/.gitignore
index ad1f6b56d..e0b9fbae6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,2 @@
*.elc
*~
-CMakeCache.txt
-CMakeFiles/
-/Makefile
-cmake_install.cmake
diff --git a/CMakeLists.txt b/CMakeLists.txt
deleted file mode 100644
index efa9092ac..000000000
--- a/CMakeLists.txt
+++ /dev/null
@@ -1,59 +0,0 @@
-cmake_minimum_required(VERSION 3.6)
-project(ledger-mode)
-
-set(EMACS_LISP_SOURCES
- ledger-check.el
- ledger-commodities.el
- ledger-complete.el
- ledger-context.el
- ledger-exec.el
- ledger-fontify.el
- ledger-fonts.el
- ledger-init.el
- ledger-mode.el
- ledger-navigate.el
- ledger-occur.el
- ledger-post.el
- ledger-reconcile.el
- ledger-regex.el
- ledger-report.el
- ledger-schedule.el
- ledger-sort.el
- ledger-state.el
- ledger-test.el
- ledger-texi.el
- ledger-xact.el)
-
-# find emacs and complain if not found
-find_program(EMACS_EXECUTABLE emacs)
-
-macro(add_emacs_lisp_target el)
- configure_file(${el} ${CMAKE_CURRENT_BINARY_DIR}/${el})
-
- # add rule (i.e. command) how to generate the byte-compiled file
- add_custom_command(
- OUTPUT ${el}c
- COMMAND ${EMACS_EXECUTABLE}
- -L ${CMAKE_CURRENT_BINARY_DIR}
- -l ${CMAKE_CURRENT_BINARY_DIR}/ledger-regex.el
- -batch -f batch-byte-compile
- ${CMAKE_CURRENT_BINARY_DIR}/${el}
- DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/${el}
- WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
- COMMENT "Creating byte-compiled Emacs lisp ${CMAKE_CURRENT_BINARY_DIR}/${el}c")
-endmacro(add_emacs_lisp_target el)
-
-if (EMACS_EXECUTABLE)
- foreach(el ${EMACS_LISP_SOURCES})
- add_emacs_lisp_target(${el})
- list(APPEND EMACS_LISP_BINARIES ${CMAKE_CURRENT_BINARY_DIR}/${el}c)
- endforeach()
-
- add_custom_target(emacs_lisp_byte_compile ALL DEPENDS ${EMACS_LISP_BINARIES})
-
- # install the byte-compiled emacs-lisp sources
- install(FILES ${EMACS_LISP_SOURCES} ${EMACS_LISP_BINARIES}
- DESTINATION share/emacs/site-lisp/ledger-mode)
-endif()
-
-### CMakeLists.txt ends here
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2c4399353..fbf30f62f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,7 +2,7 @@ Tips for contributors
---------------------
* In your local repository, ensure that everything compiles by **running
- `cmake . && make`** (this will launch byte compilation of lisp files and regression
+ `make`** (this will launch byte compilation of lisp files and regression
tests).
* You are then ready to make a **pull request**. Please make pull requests
**against `master`**.
@@ -30,6 +30,9 @@ description on GitHub.
**./LICENSE.md**: the [GPLv2] license.
+**./makem.sh** and **./Makefile**: build scripts for linting and testing this
+project, copied from [makem].
+
**./*.el**: the [Emacs] ledger-mode lisp code.
**./doc/**: documentation, and tools for generating documents such as the *pdf*
@@ -47,3 +50,4 @@ manual.
[Emacs]: http://www.gnu.org/software/emacs/
[GPLv2]: http://www.gnu.org/licenses/gpl-2.0.html
[github]: https://github.com/ledger/ledger-mode/
+[makem]: https://github.com/alphapapa/makem.sh/
diff --git a/Makefile b/Makefile
new file mode 100644
index 000000000..64c451611
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,59 @@
+# * makem.sh/Makefile --- Script to aid building and testing Emacs Lisp packages
+
+# URL: https://github.com/alphapapa/makem.sh
+# Version: 0.5
+
+# * 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 install-deps
+ INSTALL_DEPS = "--install-deps"
+endif
+ifdef install-linters
+ INSTALL_LINTERS = "--install-linters"
+endif
+
+ifdef sandbox
+ ifeq ($(sandbox), t)
+ SANDBOX = --sandbox
+ else
+ SANDBOX = --sandbox=$(sandbox)
+ endif
+endif
+
+ifdef debug
+ DEBUG = "--debug"
+endif
+
+# ** Verbosity
+
+# Since the "-v" in "make -v" gets intercepted by Make itself, we have
+# to use a variable.
+
+verbose = $(v)
+
+ifneq (,$(findstring vvv,$(verbose)))
+ VERBOSE = "-vvv"
+else ifneq (,$(findstring vv,$(verbose)))
+ VERBOSE = "-vv"
+else ifneq (,$(findstring v,$(verbose)))
+ VERBOSE = "-v"
+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 $(DEBUG) $(VERBOSE) $(SANDBOX) $(INSTALL_DEPS) $(INSTALL_LINTERS) $(@)
+
+.DEFAULT: init
+init:
+ @./makem.sh $(DEBUG) $(VERBOSE) $(SANDBOX) $(INSTALL_DEPS) $(INSTALL_LINTERS)
diff --git a/ledger-complete.el b/ledger-complete.el
index d83759f71..21f4b60c9 100644
--- a/ledger-complete.el
+++ b/ledger-complete.el
@@ -23,8 +23,7 @@
;; Functions providing payee and account auto complete.
(require 'cl-lib)
-(eval-when-compile
- (require 'subr-x))
+(eval-when-compile (require 'subr-x))
;; In-place completion support
diff --git a/ledger-mode.el b/ledger-mode.el
index cbebbd74f..b394f2574 100644
--- a/ledger-mode.el
+++ b/ledger-mode.el
@@ -30,6 +30,7 @@
;;; Code:
+(eval-when-compile (require 'subr-x))
(require 'ledger-regex)
(require 'org)
(require 'ledger-commodities)
diff --git a/ledger-occur.el b/ledger-occur.el
index 7bda973fe..bf64365c7 100644
--- a/ledger-occur.el
+++ b/ledger-occur.el
@@ -30,6 +30,7 @@
;;; Code:
(require 'cl-lib)
+(eval-when-compile (require 'subr-x))
(require 'ledger-navigate)
(defconst ledger-occur-overlay-property-name 'ledger-occur-custom-buffer-grep)
diff --git a/ledger-post.el b/ledger-post.el
index dd063e8fe..6d729f0e3 100644
--- a/ledger-post.el
+++ b/ledger-post.el
@@ -23,6 +23,7 @@
;;; Commentary:
;; Utility functions for dealing with postings.
+(eval-when-compile (require 'subr-x))
(require 'ledger-regex)
(require 'ledger-navigate)
diff --git a/ledger-reconcile.el b/ledger-reconcile.el
index db5cf1261..6e81f9e9c 100644
--- a/ledger-reconcile.el
+++ b/ledger-reconcile.el
@@ -27,6 +27,7 @@
;;; Code:
+(eval-when-compile (require 'subr-x))
(require 'easymenu)
(require 'ledger-init)
diff --git a/ledger-xact.el b/ledger-xact.el
index a2b870e73..e0a791d34 100644
--- a/ledger-xact.el
+++ b/ledger-xact.el
@@ -25,6 +25,7 @@
;;; Code:
+(eval-when-compile (require 'subr-x))
(require 'eshell)
(require 'ledger-regex)
(require 'ledger-navigate)
diff --git a/makem.sh b/makem.sh
new file mode 100755
index 000000000..c8c6b4ebd
--- /dev/null
+++ b/makem.sh
@@ -0,0 +1,1327 @@
+#!/usr/bin/env bash
+
+# * makem.sh --- Script to aid building and testing Emacs Lisp packages
+
+# URL: https://github.com/alphapapa/makem.sh
+# Version: 0.8-pre
+
+# * Commentary:
+
+# makem.sh is a script that 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.
+
+# The script can run Emacs with the developer's local Emacs
+# configuration, or with a clean, "sandbox" configuration that can be
+# optionally removed afterward. This is especially helpful when
+# upstream dependencies may have released new versions that differ
+# from those installed in the developer's personal configuration.
+
+# * License:
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# * Functions
+
+function usage {
+ cat <$file <$file <$file <"$file" <$file <"$file" <$file <&1)
+
+ # Set output file.
+ output_file=$(mktemp) || die "Unable to make output file."
+ paths_temp+=("$output_file")
+
+ # Run Emacs.
+ debug "run_emacs: ${emacs_command[@]} $@ &>\"$output_file\""
+ "${emacs_command[@]}" "$@" &>"$output_file"
+
+ # Check exit code and output.
+ exit=$?
+ [[ $exit != 0 ]] \
+ && debug "Emacs exited non-zero: $exit"
+
+ [[ $verbose -gt 1 || $exit != 0 ]] \
+ && cat $output_file
+
+ return $exit
+}
+
+# ** Compilation
+
+function batch-byte-compile {
+ debug "batch-byte-compile: ERROR-ON-WARN:$compile_error_on_warn"
+
+ [[ $compile_error_on_warn ]] && local error_on_warn=(--eval "(setq byte-compile-error-on-warn t)")
+
+ run_emacs \
+ --load "$(elisp-byte-compile-file)" \
+ "${error_on_warn[@]}" \
+ --eval "(unless (makem-batch-byte-compile) (kill-emacs 1))" \
+ "$@"
+}
+
+function byte-compile-file {
+ debug "byte-compile: ERROR-ON-WARN:$compile_error_on_warn"
+ local file="$1"
+
+ [[ $compile_error_on_warn ]] && local error_on_warn=(--eval "(setq byte-compile-error-on-warn t)")
+
+ # FIXME: Why is the line starting with "&& verbose 3" not indented properly? Emacs insists on indenting it back a level.
+ run_emacs \
+ --load "$(elisp-byte-compile-file)" \
+ "${error_on_warn[@]}" \
+ --eval "(pcase-let ((\`(,num-errors ,num-warnings) (makem-byte-compile-file \"$file\"))) (when (or (and byte-compile-error-on-warn (not (zerop num-warnings))) (not (zerop num-errors))) (kill-emacs 1)))" \
+ && verbose 3 "Compiling $file finished without errors." \
+ || { verbose 3 "Compiling file failed: $file"; return 1; }
+}
+
+# ** Files
+
+function submodules {
+ # Echo a list of submodules's paths relative to the repo root.
+ # TODO: Parse with bash regexp instead of cut.
+ git submodule status | awk '{print $2}'
+}
+
+function project-root {
+ # Echo the root of the project (or superproject, if running from
+ # within a submodule).
+ root_dir=$(git rev-parse --show-superproject-working-tree)
+ [[ $root_dir ]] || root_dir=$(git rev-parse --show-toplevel)
+ [[ $root_dir ]] || error "Can't find repo root."
+
+ echo "$root_dir"
+}
+
+function files-project {
+ # Echo a list of files in project; or with $1, files in it
+ # matching that pattern with "git ls-files". Excludes submodules.
+ [[ $1 ]] && pattern="/$1" || pattern="."
+
+ local excludes=()
+ for submodule in $(submodules)
+ do
+ excludes+=(":!:$submodule")
+ done
+
+ git ls-files -- "$pattern" "${excludes[@]}"
+}
+
+function dirs-project {
+ # Echo list of directories to be used in load path.
+ files-project-feature | dirnames
+ files-project-test | dirnames
+}
+
+function files-project-elisp {
+ # Echo list of Elisp files in project.
+ files-project 2>/dev/null \
+ | grep -E "\.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 \
+ | grep -E -v "$test_files_regexp" \
+ | filter-files-feature
+}
+
+function files-project-test {
+ # Echo list of Elisp test files.
+ files-project-elisp | grep -E "$test_files_regexp"
+}
+
+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.
+ grep -E -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
+ grep -E "^\\(provide '" "$path" &>/dev/null \
+ && echo "$path"
+ done
+}
+
+function args-load-files {
+ # For file in $@, echo "--load $file".
+ for file in "$@"
+ do
+ sans_extension=${file%%.el}
+ printf -- '--load %q ' "$sans_extension"
+ 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[@]}" ]]
+}
+
+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)" "${files_project_test[@]}" &>/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" "${files_project_test[@]}" &>/dev/null
+}
+
+function package-main-file {
+ # Echo the package's main file.
+ file_pkg=$(files-project "*-pkg.el" 2>/dev/null)
+
+ if [[ $file_pkg ]]
+ then
+ # Use *-pkg.el file if it exists.
+ echo "$file_pkg"
+ else
+ # Use shortest filename (a sloppy heuristic that will do for now).
+ for file in "${files_project_feature[@]}"
+ do
+ echo ${#file} "$file"
+ done \
+ | sort -h \
+ | head -n1 \
+ | sed -r 's/^[[:digit:]]+ //'
+ fi
+}
+
+function dependencies {
+ # Echo list of package dependencies.
+
+ # Search package headers. Use -a so grep won't think that an Elisp file containing
+ # control characters (rare, but sometimes necessary) is binary and refuse to search it.
+ grep -E -a -i '^;; Package-Requires: ' $(files-project-feature) $(files-project-test) \
+ | grep -E -o '\([^([:space:]][^)]*\)' \
+ | grep -E -o '^[^[:space:])]+' \
+ | sed -r 's/\(//g' \
+ | grep -E -v '^emacs$' # Ignore Emacs version requirement.
+
+ # Search Cask file.
+ if [[ -r Cask ]]
+ then
+ grep -E '\(depends-on "[^"]+"' Cask \
+ | sed -r -e 's/\(depends-on "([^"]+)".*/\1/g'
+ fi
+
+ # Search -pkg.el file.
+ if [[ $(files-project "*-pkg.el" 2>/dev/null) ]]
+ then
+ sed -nr 's/.*\(([-[:alnum:]]+)[[:blank:]]+"[.[:digit:]]+"\).*/\1/p' $(files-project- -- -pkg.el 2>/dev/null)
+ fi
+}
+
+# ** Sandbox
+
+function sandbox {
+ verbose 2 "Initializing sandbox..."
+
+ # *** Sandbox arguments
+
+ # MAYBE: Optionally use branch-specific sandbox?
+
+ # Check or make user-emacs-directory.
+ if [[ $sandbox_dir ]]
+ then
+ # Directory given as argument: ensure it exists.
+ 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."
+ paths_temp+=("$sandbox_dir")
+ fi
+
+ # Make argument to load init file if it exists.
+ init_file="$sandbox_dir/init.el"
+
+ # 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 package-user-dir (expand-file-name \"elpa\" user-emacs-directory))"
+ --eval "(setq user-init-file (file-truename \"$init_file\"))"
+ )
+
+ # Add package-install arguments for dependencies.
+ if [[ $install_deps ]]
+ then
+ local deps=($(dependencies))
+ debug "Installing dependencies: ${deps[@]}"
+
+ # Ensure built-in packages get upgraded to newer versions from ELPA.
+ args_sandbox_package_install+=(--eval "(setq package-install-upgrade-built-in t)")
+
+ for package in "${deps[@]}"
+ do
+ args_sandbox_package_install+=(--eval "(package-install '$package)")
+ 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 'elsa)"
+ --eval "(package-install 'package-lint)"
+ --eval "(package-install 'relint)")
+ fi
+
+ # *** Install packages into sandbox
+
+ if [[ ${args_sandbox_package_install[@]} ]]
+ then
+ # Initialize the sandbox (installs packages once rather than for every rule).
+ verbose 1 "Installing packages into sandbox..."
+
+ run_emacs \
+ --eval "(setq package-user-dir (expand-file-name \"elpa\" user-emacs-directory))" \
+ -l "$package_initialize_file" \
+ --eval "(package-refresh-contents)" \
+ "${args_sandbox_package_install[@]}" \
+ && success "Packages installed." \
+ || die "Unable to initialize sandbox."
+ fi
+
+ verbose 2 "Sandbox initialized."
+}
+
+function args-load-path-sandbox {
+ # Echo list of Emacs arguments to add paths of packages installed
+ # in sandbox to load-path.
+ if ! [[ -d "$sandbox_dir/elpa" ]]
+ then
+ warn "Sandbox's \"elpa/\" directory not found: no packages installed."
+ else
+ for path in $(find "$sandbox_dir/elpa" -maxdepth 1 -type d -not -name "archives" -print \
+ | tail -n+2)
+ do
+ printf -- '-L %q ' "$path"
+ done
+ fi
+}
+
+# ** Utility
+
+function cleanup {
+ # Remove temporary paths (${paths_temp[@]}).
+
+ for path in "${paths_temp[@]}"
+ do
+ if [[ $debug ]]
+ then
+ debug "Debugging enabled: not deleting temporary path: $path"
+ elif [[ -r $path ]]
+ then
+ rm -rf "$path"
+ else
+ debug "Temporary path doesn't exist, not deleting: $path"
+ fi
+ done
+}
+
+function echo-unset-p {
+ # Echo 0 if $1 is set, otherwise 1. IOW, this returns the exit
+ # code of [[ $1 ]] as STDOUT.
+ [[ $1 ]]
+ echo $?
+}
+
+function ensure-package-available {
+ # If package $1 is available, return 0. Otherwise, return 1, and
+ # if $2 is set, give error otherwise verbose. Outputting messages
+ # here avoids repetition in callers.
+ local package=$1
+ local direct_p=$2
+
+ if ! run_emacs --load $package &>/dev/null
+ then
+ if [[ $direct_p ]]
+ then
+ error "$package not available."
+ else
+ verbose 2 "$package not available."
+ fi
+ return 1
+ fi
+}
+
+function ensure-tests-available {
+ # If tests of type $1 (like "ERT") are available, return 0. Otherwise, if
+ # $2 is set, give an error and return 1; otherwise give verbose message. $1
+ # should have a corresponding predicate command, like ert-tests-p for ERT.
+ local test_name=$1
+ local test_command="${test_name,,}-tests-p" # Converts name to lowercase.
+ local direct_p=$2
+
+ if ! $test_command
+ then
+ if [[ $direct_p ]]
+ then
+ error "$test_name tests not found."
+ else
+ verbose 2 "$test_name tests not found."
+ fi
+ return 1
+ fi
+}
+
+function echo_color {
+ # This allows bold, italic, etc. without needing a function for
+ # each variation.
+ local color_code="COLOR_$1"
+ shift
+
+ if [[ $color ]]
+ then
+ echo -e "${!color_code}${@}${COLOR_off}"
+ else
+ echo "$@"
+ fi
+}
+function debug {
+ if [[ $debug ]]
+ then
+ function debug {
+ echo_color yellow "DEBUG ($(ts)): $@" >&2
+ }
+ debug "$@"
+ else
+ function debug {
+ true
+ }
+ fi
+}
+function error {
+ echo_color red "ERROR ($(ts)): $@" >&2
+ ((errors++))
+ return 1
+}
+function die {
+ [[ $@ ]] && error "$@"
+ exit $errors
+}
+function warn {
+ echo_color yellow "WARNING ($(ts)): $@" >&2
+ ((warnings++))
+}
+function log {
+ echo "LOG ($(ts)): $@" >&2
+}
+function log_color {
+ local color_name=$1
+ shift
+ echo_color $color_name "LOG ($(ts)): $@" >&2
+}
+function success {
+ if [[ $verbose -ge 2 ]]
+ then
+ log_color green "$@" >&2
+ fi
+}
+function verbose {
+ # $1 is the verbosity level, rest are echoed when appropriate.
+ if [[ $verbose -ge $1 ]]
+ then
+ [[ $1 -eq 1 ]] && local color_name=blue
+ [[ $1 -eq 2 ]] && local color_name=cyan
+ [[ $1 -ge 3 ]] && local color_name=white
+
+ shift
+ log_color $color_name "$@" >&2
+ fi
+}
+
+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.
+# Some rules test $1 to determine whether the rule is being called
+# directly or from a meta-rule; if directly, an error is given if the
+# rule can't be run, otherwise it's skipped.
+
+function all {
+ verbose 1 "Running all rules..."
+
+ lint
+ tests
+}
+
+function compile-batch {
+ [[ $compile ]] || return 0
+ unset compile # Only compile once.
+
+ verbose 1 "Compiling..."
+ verbose 2 "Batch-compiling files..."
+ debug "Byte-compile files: ${files_project_byte_compile[@]}"
+
+ batch-byte-compile "${files_project_byte_compile[@]}"
+}
+
+function compile-each {
+ [[ $compile ]] || return 0
+ unset compile # Only compile once.
+
+ verbose 1 "Compiling..."
+ debug "Byte-compile files: ${files_project_byte_compile[@]}"
+
+ local compile_errors
+ for file in "${files_project_byte_compile[@]}"
+ do
+ verbose 2 "Compiling file: $file..."
+ byte-compile-file "$file" \
+ || compile_errors=t
+ done
+
+ [[ ! $compile_errors ]]
+}
+
+function compile {
+ if [[ $compile = batch ]]
+ then
+ compile-batch "$@"
+ else
+ compile-each "$@"
+ fi
+ local status=$?
+
+ if [[ $compile_error_on_warn ]]
+ then
+ # Linting: just return status code, because lint rule will print messages.
+ [[ $status = 0 ]]
+ else
+ # Not linting: print messages here.
+ [[ $status = 0 ]] \
+ && success "Compiling finished without errors." \
+ || error "Compiling failed."
+ fi
+}
+
+function batch {
+ # Run Emacs in batch mode with ${args_batch_interactive[@]} and
+ # with project source and test files loaded.
+ verbose 1 "Executing Emacs with arguments: ${args_batch_interactive[@]}"
+
+ run_emacs \
+ $(args-load-files "${files_project_feature[@]}" "${files_project_test[@]}") \
+ "${args_batch_interactive[@]}"
+}
+
+function interactive {
+ # Run Emacs interactively. Most useful with --sandbox and --install-deps.
+ local load_file_args=$(args-load-files "${files_project_feature[@]}" "${files_project_test[@]}")
+ verbose 1 "Running Emacs interactively..."
+ verbose 2 "Loading files: ${load_file_args//--load /}"
+
+ [[ $compile ]] && compile
+
+ unset arg_batch
+ run_emacs \
+ $load_file_args \
+ --eval "(load user-init-file)" \
+ "${args_batch_interactive[@]}"
+ arg_batch="--batch"
+}
+
+function lint {
+ verbose 1 "Linting..."
+
+ lint-checkdoc
+ lint-compile
+ lint-declare
+ # NOTE: Elint doesn't seem very useful at the moment. See comment
+ # in lint-elint function.
+ # lint-elint
+ lint-indent
+ lint-package
+ lint-regexps
+}
+
+function lint-checkdoc {
+ verbose 1 "Linting checkdoc..."
+
+ local checkdoc_file="$(elisp-checkdoc-file)"
+ paths_temp+=("$checkdoc_file")
+
+ run_emacs \
+ --load="$checkdoc_file" \
+ "${files_project_feature[@]}" \
+ && success "Linting checkdoc finished without errors." \
+ || error "Linting checkdoc failed."
+}
+
+function lint-compile {
+ verbose 1 "Linting compilation..."
+
+ compile_error_on_warn=true
+ compile "${files_project_byte_compile[@]}" \
+ && success "Linting compilation finished without errors." \
+ || error "Linting compilation failed."
+ 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-elsa {
+ verbose 1 "Linting with Elsa..."
+
+ # MAYBE: Install Elsa here rather than in sandbox init, to avoid installing
+ # it when not needed. However, we should be careful to be clear about when
+ # packages are installed, because installing them does execute code.
+ run_emacs \
+ --load elsa \
+ -f elsa-run-files-and-exit \
+ "${files_project_feature[@]}" \
+ && success "Linting with Elsa finished without errors." \
+ || error "Linting with Elsa failed."
+}
+
+function lint-elint {
+ # NOTE: Elint gives a lot of spurious warnings, apparently because it doesn't load files
+ # that are `require'd, so its output isn't very useful. But in case it's improved in
+ # the future, and since this wrapper code already works, we might as well leave it in.
+ verbose 1 "Linting with Elint..."
+
+ local errors=0
+ for file in "${files_project_feature[@]}"
+ do
+ verbose 2 "Linting with Elint: $file..."
+ run_emacs \
+ --load "$(elisp-elint-file)" \
+ --eval "(makem-elint-file \"$file\")" \
+ && verbose 3 "Linting with Elint found no errors." \
+ || { error "Linting with Elint failed: $file"; ((errors++)) ; }
+ done
+
+ [[ $errors = 0 ]] \
+ && success "Linting with Elint finished without errors." \
+ || error "Linting with Elint failed."
+}
+
+function lint-indent {
+ verbose 1 "Linting indentation..."
+
+ # We load project source files as well, because they may contain
+ # macros with (declare (indent)) rules which must be loaded to set
+ # indentation.
+
+ run_emacs \
+ --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."
+}
+
+function lint-package {
+ ensure-package-available package-lint $1 || return $(echo-unset-p $1)
+
+ verbose 1 "Linting package..."
+
+ run_emacs \
+ --load package-lint \
+ --eval "(setq package-lint-main-file \"$(package-main-file)\")" \
+ --funcall package-lint-batch-and-exit \
+ "${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..."
+
+ test-ert
+ 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
+
+ verbose 1 "Running Buttercup tests..."
+
+ local buttercup_file="$(elisp-buttercup-file)"
+ paths_temp+=("$buttercup_file")
+
+ run_emacs \
+ $(args-load-files "${files_project_test[@]}") \
+ --load "$buttercup_file" \
+ --eval "(progn (setq backtrace-on-error-noninteractive nil) (buttercup-run))" \
+ && success "Buttercup tests finished without errors." \
+ || error "Buttercup tests failed."
+}
+
+function test-ert {
+ ensure-tests-available ERT $1 || return $(echo-unset-p $1)
+ compile || die
+
+ verbose 1 "Running ERT tests..."
+ debug "Test files: ${files_project_test[@]}"
+
+ run_emacs \
+ $(args-load-files "${files_project_test[@]}") \
+ -f ert-run-tests-batch-and-exit \
+ && success "ERT tests finished without errors." \
+ || error "ERT tests failed."
+}
+
+# * Defaults
+
+test_files_regexp='^((tests?|t)/)|-tests?.el$|^test-'
+
+emacs_command=("emacs")
+errors=0
+# TODO: Do something with number of warnings?
+warnings=0
+verbose=0
+compile=true
+arg_batch="--batch"
+compile=each
+
+# 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
+
+# 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.
+
+# ** Colors
+
+COLOR_off='\e[0m'
+COLOR_black='\e[0;30m'
+COLOR_red='\e[0;31m'
+COLOR_green='\e[0;32m'
+COLOR_yellow='\e[0;33m'
+COLOR_blue='\e[0;34m'
+COLOR_purple='\e[0;35m'
+COLOR_cyan='\e[0;36m'
+COLOR_white='\e[0;37m'
+
+# ** Package system args
+
+args_package_archives=(
+ --eval "(add-to-list 'package-archives '(\"gnu\" . \"https://elpa.gnu.org/packages/\") t)"
+ --eval "(add-to-list 'package-archives '(\"melpa\" . \"https://melpa.org/packages/\") t)"
+)
+
+args_package_init=(
+ --eval "(package-initialize)"
+)
+
+# * Args
+
+args=$(getopt -n "$0" \
+ -o dhce:E:i:s::vf:C \
+ -l compile-batch,exclude:,emacs:,install-deps,install-linters,debug,debug-load-path,help,install:,verbose,file:,no-color,no-compile,sandbox:: \
+ -- "$@") \
+ || { usage; exit 1; }
+eval set -- "$args"
+
+while true
+do
+ case "$1" in
+ --install-deps)
+ install_deps=true
+ ;;
+ --install-linters)
+ 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
+ ;;
+ -h|--help)
+ usage
+ exit
+ ;;
+ -c|--compile-batch)
+ debug "Compiling files in batch mode"
+ compile=batch
+ ;;
+ -E|--emacs)
+ shift
+ emacs_command=($1)
+ ;;
+ -i|--install)
+ shift
+ args_sandbox_package_install+=(--eval "(package-install '$1)")
+ ;;
+ -s|--sandbox)
+ sandbox=true
+ shift
+ sandbox_dir="$1"
+
+ if ! [[ $sandbox_dir ]]
+ then
+ debug "No sandbox dir: installing dependencies."
+ install_deps=true
+ else
+ debug "Sandbox dir: $1"
+ fi
+ ;;
+ -v|--verbose)
+ ((verbose++))
+ ;;
+ -e|--exclude)
+ shift
+ debug "Excluding file: $1"
+ files_exclude+=("$1")
+ ;;
+ -f|--file)
+ shift
+ args_files+=("$1")
+ ;;
+ --no-color)
+ unset color
+ ;;
+ -C|--no-compile)
+ unset compile
+ ;;
+ --)
+ # Remaining args (required; do not remove)
+ shift
+ rest=("$@")
+ break
+ ;;
+ esac
+
+ shift
+done
+
+debug "ARGS: $args"
+debug "Remaining args: ${rest[@]}"
+
+# Set package elisp (which depends on --no-org-repo arg).
+package_initialize_file="$(elisp-package-initialize-file)"
+paths_temp+=("$package_initialize_file")
+
+# * Main
+
+trap cleanup EXIT INT TERM
+
+# Change to project root directory first.
+cd "$(project-root)"
+
+# 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[@]}")
+
+if [[ ${args_files[@]} ]]
+then
+ # Add specified files.
+ files_project_feature+=("${args_files[@]}")
+ files_project_byte_compile+=("${args_files[@]}")
+fi
+
+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[@]}"
+debug "PACKAGE-MAIN-FILE: $(package-main-file)"
+
+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))
+
+# 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
+ args_load_paths+=($(args-load-path-sandbox))
+}
+
+debug "LOAD PATH ARGS: ${args_load_paths[@]}"
+
+# Run rules.
+for rule in "${rest[@]}"
+do
+ if [[ $batch || $interactive ]]
+ then
+ 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
+ # being called directly rather than from a meta-rule.
+ $rule called-directly
+ elif [[ $rule = test ]]
+ then
+ # Allow the "tests" rule to be called as "test". Since "test"
+ # is a shell builtin, this workaround is required.
+ tests
+ else
+ error "Invalid rule: $rule"
+ fi
+done
+
+# Batch/interactive rules.
+[[ $batch ]] && batch
+[[ $interactive ]] && interactive
+
+if [[ $errors -gt 0 ]]
+then
+ log_color red "Finished with $errors errors."
+else
+ success "Finished without errors."
+fi
+
+exit $errors
diff --git a/test/Makefile b/test/Makefile
index cc722f66c..1d07d7079 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -7,8 +7,6 @@ EL := $(wildcard *.el)
ELC := $(patsubst %.el,%.elc,$(EL))
ERT := $(filter-out test-helper.el,$(EL))
-CHECKDOC_BATCH_EL := ../tools/checkdoc-batch.el
-
.PHONY: all
all: compile test
@@ -28,14 +26,10 @@ clean:
.PHONY: distclean
distclean: clean
- rm -f $(CHECKDOC_BATCH_EL)
-
-$(CHECKDOC_BATCH_EL):
- wget --no-verbose https://download.tuxfamily.org/user42/checkdoc-batch.el --output-document=$(CHECKDOC_BATCH_EL)
.PHONY: checkdoc
-checkdoc: $(CHECKDOC_BATCH_EL)
- $(EMACS_BATCH) --load $(CHECKDOC_BATCH_EL) --funcall checkdoc-batch-commandline ../*.el | grep -E "el:[0-9]+:[0-9]+:" && exit 1 || exit 0
+checkdoc:
+ make -C ../ lint-checkdoc
# Enables `make ledger-fontify/test-003`
define ERTDEFTEST