akirak / elinter

Nix-based CI and local testing framework for Emacs Lisp projects
GNU General Public License v3.0
18 stars 2 forks source link
emacs-lisp linting nix testing

This is a complete rewrite of my Emacs Lisp package checker, formerly called =emacs-package-checker= or =melpa-check=. It is still currently at an alpha stage.

+begin_html

Build Status

Build Status

+end_html

** Table of contents :PROPERTIES: :TOC: siblings :END:

** Project goals This project aims at the following goals:

Linting (the list mostly extracted from [[https://github.com/alphapapa/makem.sh][makem.sh]] + inspiration from [[https://github.com/gonewest818/elisp-lint][elisp-lint]]):

Building packages:

Testing:

Target project types:

Sources:

Configuration:

Integration:

Reporting:

GitHub Actions:

+begin_src sh

bash <(curl -L https://nixos.org/nix/install)

+end_src

=elinter= can be installed using Nix:

+begin_src sh

nix-env -if https://github.com/akirak/elinter/archive/v4.tar.gz

+end_src

Alternatively, you can clone this repository and run

+begin_src sh

nix-env -if .

+end_src

Optionally, it is recommended to install [[https://github.com/cachix/cachix][cachix]] and enable cached Emacs binaries:

+begin_src sh

cachix use emacs-ci

+end_src

Optionally, you can use [[https://github.com/xzfc/cached-nix-shell][cached-nix-shell]] for improving =nix-shell= startup time. Install the program in =PATH=, and =elinter= will automatically detect it. ** Usage *** Use case #1: Local source + in-repository recipes First create package recipes in =.recipes= directory. The recipe format is [[https://github.com/melpa/melpa/#recipe-format][the same as you would create for MELPA]], and each file should define exactly one package.

You can use =elinter.el=, which is included in this project, to copy existing recipes from your local copy of MELPA. First set =elinter-recipes-dir= to the =recipes= directory inside MELPA, and then use =elinter= interactive function. It scans source files in the repository, import package recipes, and run lint on source files.

The below describes command line usage but also applies to =elinter= command inside Emacs.

Without arguments, it lints and compiles source files in the repository:

+begin_src sh

elinter

+end_src

To enable experimental checks by [[https://github.com/riscy/melpazoid/][melpazoid]] which you would receive on MELPA PRs, add =--experimental= flag:

+begin_src sh

elinter --experimental

+end_src

With =--buttercup= or =--ert-runner= flag, it also runs tests:

+begin_src sh

elinter --buttercup

+end_src

To only lint packages and prevent from byte-compiling, add =-l= flag:

+begin_src sh

elinter -l

+end_src

To only byte-compile source files, add =-b= flag:

+begin_src sh

elinter -b

+end_src

To only run tests, run it with =-t= along with the =--buttercup=, =--ert-runner=, etc.:

+begin_src sh

elinter -t --buttercup

+end_src

See [[https://github.com/akirak/elinter-demo/blob/master/.github/workflows/test.yml][elinter-demo]] and [[https://github.com/akirak/elinter-demo/actions?query=workflow%3ADemo][its status page]] for concrete examples. *** Use case #2: Local recipes + remote sources This is like the use case 1, but it clones the remote repository specified in the recipe rather than work on source files in the working directory.

=elinter= command accepts recipe files as arguments. When =-r= flag is given, it clones remote Git repositories according to the recipes. This can be easily integrated into the MELPA PR workflow:

+begin_src sh

cd ~/your-melpa-fork elinter -r recipes/your-package --experimental

+end_src

The same flags as #1 applies. *** Use case #3: GitHub workflow for package repositories This repository also provides a GitHub action for checking individual packages on GitHub.

The following is an example workflow. Create a file in =.github/workflows=. Here is [[https://github.com/akirak/elinter/actions?query=workflow%3A%22Action+CI%22][an example output]].

+begin_src yaml

name: CI on: push: jobs: ci: runs-on: ubuntu-latest steps:

Here is [[https://github.com/akirak/melpa/blob/internal/.github/workflows/akirak.yml][an example workflow definition]] and [[https://github.com/akirak/melpa/actions?query=workflow%3ACI][output]]. *** Use case #5: Git pre-commit hook This repository also provides a script that can be integrated into Git =pre-commit= hook.

If you run =elinter= with =-g= argument at a repository root, it installs a =pre-commit= hook that performs static checks based on the recipe(s).

It uses [[https://pre-commit.com/][pre-commit]], and =.pre-commit-config.yaml= is created at the repository root.

Note that byte-compilation is not performed by the hook.

Alternatively, you can use [[cachix/pre-commit-hooks.nix][cachix/pre-commit-hooks.nix]] to configure the hook for multiple languages in Nix. The following is an example:

+begin_src nix

with builtins; with (import {}); with (import (import ./nix/sources.nix).gitignore { }); let pre-commit-hooks = import (import ./nix/sources.nix)."pre-commit-hooks.nix";

elinter = import (fetchTarball "https://github.com/akirak/elinter/archive/v4.tar.gz") { };

pre-commit-check = pre-commit-hooks.run {
  src = gitignoreSource ../.;
  excludes = [ "^nix/sources\.nix$" ];
  hooks = {
    shellcheck.enable = true;
    nix-linter.enable = true;
    nixpkgs-fmt.enable = true;
    elinter = {
      enable = true;
      name = "elinter";
      description = "Lint Emacs Lisp files";
      entry = "${elinter.file-linter}/bin/elinter-lint-files";
      files = "\\.el$";
    };
  };
};

in mkShell { shellHook = pre-commit-check.shellHook; }

+end_src

That is, =file-linter= Nix attribute of this repository provides =elinter-lint-files= executable which performs static checks on given files, so you can integrate it using any Git hooks manager:

+begin_src sh

install the script

nix-env -if . -A file-linter

check source files in your repository

elinter-lint-files hello.el hello-utils.el

+end_src

** Technical details *** elinter command The default Nix derivation provides =elinter= executable. It takes recipe files as command line arguments.

It can also take package names and it refers to source files linked from the sandbox (described below).

If no recipe file or package name is given as an argument, it looks for ones in =.recipes= directory in the working directory. *** Emacs version syntax =elinter= command supports =-e= option that takes an Emacs version, e.g. =26.3= for Emacs 26.3 and =snapshot= for the latest snapshot. You can use any single version available in nix-emacs-ci.

It also supports the following abstract version specs:

+begin_src sh

elinter -u RECIPES

+end_src

*** Sandboxing =elinter= creates symbolic links in a cache directory and operates on them, rather than lint and compile source files directly in the repository. This is useful both for simplification and isolation. Once symbolic links are created, they are reused across different runs for performance. Since they are symbolic links, file modifications are reflected, but file additions/deletions are not applied. After you create/delete a source file in the repository, you have to run =elinter= with =-c= arguments to recreate the sandbox:

+begin_src sh

elinter -c

+end_src

You can use =-c= with other arguments:

+begin_src sh

elinter -c -l --experimental

+end_src

** Credits, inspiration sources, and alternatives =elinter= was influenced by or depends on the following projects:

=elinter= is an improvement upon the previous version, on which I gained help by [[https://github.com/ericdallo][Eric Dallo]] and [[https://github.com/terlar][Terje Larsen]].

The current version (v4) was contributed by the following people: