Skip to content

emacs-twist/rice-config

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

72 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Nix Meta-Flake for Developing Emacs Lisp Projects

This is a Nix flake that lets you configure checking of Emacs Lisp packages (both locally and on CI).

There are several alternatives for Emacs Lisp CI, but this project is unique in that it is powered by Nix and provides the following most notable features:

  • In continuous integration, this project makes it easy to run byte-compile (and testing in the future) on all Emacs versions supported by your package(s). It is aimed at authors who maintain a number of Emacs Lisp packages.
  • In local usage, it can integrate with Nix Git hooks, so it even nicely supports projects that contain multiple programming languages.

This is a meta-framework which allows you to reuse a single flake across multiple projects. You can fork this repository to personalize your workflows and then use it in all of your Emacs Lisp projects. If your project is complex enough to require a specific flake configuration, you can copy the flake.nix from this repository and use it in the single repository. The common code is maintained in a separate repository as a flake-parts module, so you don’t have to merge upstream bugfixes by hand.

To use this project, you will need some experience with Nix, but the resulting configuration of each project will be extremely concise.

Installation

To use this flake on your machine, you need the following requirements:

  • Git (required)
  • Nix with flakes enabled (required)
  • just (optional but strongly recommended for usability)
  • GitHub CLI, a.k.a. gh (optional but useful in some situations)

(Optional) Fork this repository

Because of security concerns, it is discouraged to use this rice-config repository directly. Instead, I would recommend that you fork the repository and add flake.lock to it.

This repository depends on third-party repositories via its flake inputs. The inputs are not locked, which is open to supply-chain attacks. The upstream projects of this flake are well trusted in the Nix community, and they should be safe as long as you run pure evaluation of Nix. However, I am irresponsible for any security problem caused by any of the projects maintained outside of this organization. Also note that this repository may add new dependencies in the future. In such a situation, you won’t be notified of the change beforehand, if you use the flake without locking the input. For the reasons described above, I would recommend tracking the inputs for yourself, rather than to be at stake of unknown third-party developers. The maintenance effort would be near constant no matter how many projects you maintain, given that you will have only one flake for all of them.

First fork this repository on GitHub and clone it to your computer:

git clone github:YOUR-USER/rice-config

Then generate flake.lock:

cd rice-config
nix flake lock

Commit the lock file and push it to your forked repository.

Create a MELPA recipe for your package

Twist, the Nix library of Rice, primarily supports MELPA, and Rice currently targets projects that are submitted to MELPA.

If your package is not available on MELPA yet, you have to create a MELPA recipe as if you are planning on submission in the future.

  1. First fork MELPA on your GitHub account.
  2. Create a recipe for your package and save it to recipes/PACKAGE-NAME in the repository. The recipe format is S-exp-based, and you can edit it using lisp-data-mode on Emacs.
  3. Commit the recipe and push it to a branch on your remote repository.

An example of the most basic recipe is as follows:

(hello :fetcher github :repo "your-user/hello.el")

Add flake.nix and (optional) justfile

You have to add flake.nix to your individual projects. While it is possible to use its flake API only with nix command, the commands are quite verbose and hence your developer experience would be painful. On your machine, it is recommended to use just (which is like a modern Make for running commands) for the development workflow.

I have created a flake template that provides a boilerplate with flake.nix, justfile, and some other typical configuration files. With this template, you can get started with Rice just by editing parameters.

Using the template, you can add the configuration files to your existing project by running the following command:

nix flake init -t "github:emacs-twist/rice-config?dir=templates"

Because of the ? character, you probably have to quote the URL, though it depends on your shell.

flake.nix in an individual project is just an input to this flake (i.e. rice-config), and it is designed to be minimal. You can initialize the file from the flake template, you only have to set elisp-rice.packages in the outputs. A minimal example is as follows:

{
  outputs = {...}: {
    elisp-rice = {
      packages = ["hello"];
    };
  };
}

In this example, hello is the name of your package. If your package contains multiple source files, you only specify the main library. If your repository contains multiple packages, you have to specify all of them in packages.

You can also set up tests by setting tests:

{
  outputs = {...}: {
    elisp-rice = {
      packages = ["hello"];
      tests = {
        buttercup.enable = true;
      };
    };
  };
}

Open justfile to adapt it to your project. You only need to edit variables. You must set package, and you may also have to set rice-config, melpa, and emacs-version as well.

The justfile only supports a single package and a single Emacs version, but you can override it at runtime using --set option of just or even edit justfile without committing it to the repository.

(Optional) Set up a CI on GitHub Actions

We provide reusable GitHub workflows for running common checks on CI. Check out elisp-workflows repository. You can call the workflows directly, or fork the repository and maintain it for yourself, if you worry about security. Note that the workflows don’t require the justfile.

Usage

Once your project is set up, this rice-config repository (or its fork) will become a Nix flake that provides the interface. You will use the flake with the following inputs overridden for your project:

rice-src
The source repository of your project.
rice-lock
The lock directory tracking the Emacs Lisp dependencies of your project. This is optional if your project only depends on built-in libraries.
melpa
A repository containing the recipe for your project.

You *could*​ browse the flake interface using nix flake show as follows:

nix flake show github:your-user/rice-config \
    --override-input rice-src "path:$PWD" \
    --override-input rice-lock "path:$PWD/.rice-lock/default" \
    --override-input melpa github:your-user/melpa/your-package

Note the above command (or any evaluation on this flake) *doesn’t work*​ if the lock directory has a missing dependency. See the next subsection on how to initialize the lock directory.

However, it is tedious to repeat the --override-input options for all of the commands, so the justfile provides the following shorthand:

just show

just show also accepts Nix options:

just show --json

Another way to inspect the flake is nix eval, and the justfile provides a shorthand for the command:

just eval melpaRecipes --apply builtins.attrNames --json

You can browse the recipes in justfile from the command line:

just -l

Initializing a lock directory

The justfile contains a recipe for initializing a lock directory.

just lock

The lock directory will be created in lock-dir defined in the justfile. By default, the location is .rice-lock/default, but you can create multiple lock directories by overriding the variable:

just --set lock-dir .rice-lock/secondary lock

However, it is recommended to use the minimum version of Emacs supported by your package to generate a lock directory that contains all dependencies:

just --set emacs emacs-lowest lock

The above command generates a lock directory that contains all dependencies to make your package run on the minimum supported version of Emacs. Generally speaking, a newer version of Emacs has more built-in packages, so it’s safe to use the oldest version of Emacs. You can still create multiple versions of lock directories.

Note: You can set emacs to any package provided by nix-emacs-ci. This includes stable release versions (e.g. emacs-28-1) and snapshot versions (i.e. emacs-snapshot and emacs-release-snapshot). Furthermore, rice adds an aliased version named emacs-lowest which points to the minimum Emacs version supported by your package(s).

To override an existing directory, you need to set --force option:

just --set emacs emacs-lowest lock --force

The lock command supports some other options. You can view the help:

just lock --help

Byte-compile

In recent years, the byte-compiler of Emacs has made a significant advancement, and it is one of the most useful tools for linting Emacs Lisp code. Rice primarily aims at integrating the byte-compiler into your development workflow.

Running byte-compile on a specific Emacs version

just check-compile

This compiles all source files in the package in a sandboxed environment of Nix. It is generally suitable for CI, but you can run it locally for faster feedback, without waiting for CI.

It runs nix build on one of the outputs under checks (e.g. #checks.x86_64-linux.hello-compile-emacs-29-4) with the inputs overridden.

Shell for byte-compiling

just shell-compile

Once you enter the shell, you can byte-compile individual source files by running a wrapper script:

elisp-byte-compile SOURCE..

This is more suitable during development, because you can re-run the command after you edit a source file without reloading the Nix environment. However, I would recommend use of watch mode, which I will describe later.

Technically, this justfile recipe is a wrapper for one of the outputs under devShells (e.g. devShells.x86_64-linux.emacs-29-4-for-hello). The shell environment provides Emacs with the package dependencies (but not the package itself).

Watch mode

You can re-run byte-compile whenever a file is changed:

just watch-compile

It enters the same shell as just shell-compile but runs a command in the shell. Internally, it uses entr to watch file changes. entr is bundled in the shell but light on size.

Testing (manual/automated)

Testing should be done on all supported Emacs versions, which can be tedious for package authors and maintainers. This is another area where Rice is trying to improve.

If you have set up a test suite via tests output of the flake, you can run tests using test-* recipe:

just test-buttercup

It runs test-buttercup-with-emacs-snapshot package of the flake with the inputs.

Shell with a specific Emacs version

Instead of running the pre-configured test suite for your package, it is also possible to run a given command in a package-enabled Emacs environment.

You can enter a shell with the package(s) available:

just shell-emacs

You can run emacs -q to start Emacs without your init file loaded. That environment will be suitable for manual testing your package in a reproducible manner. You can also run Emacs with -batch flag to dispatch automated testing.

This enters a shell environment with one of the packages under packages (e.g. packages.x86_64-linux.emacs-29-4-with-packages). The recipe accepts options, which are passed to nix shell, so you can run a test suite directly:

just shell-emacs --command emacs -batch -L tests -l hello-test

Plans

This project does not support all common types of checking in Emacs Lisp yet. The following should be covered in the future:

  • package-lint: This requires package.el to download dependencies, so it requires internet connection, which cannot be run in a pure Nix environment. It should be an application provided under packages output of the flake.
  • checkdoc and other minor checks that can be run statically: This should be an optional addition to checks. Only one Emacs version (either the latest release or a snapshot version) would be enough.
  • Add support for more test packages, e.g. ERT.

The above tasks are likely to require enhancement of the rice module.

Alternatives

As mentioned earlier, several alternatives do exist for checking Emacs Lisp packages. For information on eariler projects, see comparisons by the author of makem.sh. Below is a list of some (but not comprehensive) recent projects:
  • eask, which seems actively developed and reliable
  • melpazoid by one of the maintainers of MELPA
  • elisp-check, which works great on GitHub Actions

Rice doesn’t aim to be a superset/competitor of these projects. It is:

  • Built with Nix (and twist.nix) to support reproducibility, integration with native (non Emacs Lisp) dependencies, avoiding S-exp (or any other custom) DSLs. The nature of defining everything in Nix would allow end-to-end/UI testing involving the Emacs Lisp package(s) under test.
  • Integrating nix-emacs-ci to support generating an up-to-date CI matrix, without needing manual configuration.
  • Designed to help the user maintain a number of Emacs Lisp projects, by centralizing the configuration in a single place.

Rice is an immature project and does not support as many checks as earlier projects. Because of this, I won’t provide a list of feature comparisons yet.

Contributing

An improvement or enhancement to this flake is welcome. Please feel free to open a ticket to request a feature or send a PR to implement a planned feature.

About

A Nix flake for Emacs Lisp CI configuration

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published