project management tool for emacs lisp
  • Emacs Lisp 99.5%
  • Shell 0.5%
Find a file
2026-01-10 20:43:50 -05:00
.beads init beads 2025-11-24 17:29:30 -05:00
docs/plans implement running tasks in different emacsen 2026-01-05 09:21:36 -05:00
test implement running tasks in different emacsen 2026-01-05 09:21:36 -05:00
.gitignore chore: add .elk/ to gitignore 2025-11-27 22:58:49 -05:00
elk working prototype 2025-11-24 20:26:11 -05:00
elk-cli.el implement running tasks in different emacsen 2026-01-05 09:21:36 -05:00
elk-container.el implement running tasks in different emacsen 2026-01-05 09:21:36 -05:00
elk-core.el feat(core): add scope field to tasks (prod/dev/test) 2025-11-27 22:31:54 -05:00
elk-deps.el fix elpaca bootstrap bug 2026-01-10 20:43:50 -05:00
elk-discover.el working prototype 2025-11-24 20:26:11 -05:00
elk-runner.el implement running tasks in different emacsen 2026-01-05 09:21:36 -05:00
elk-task-clean.el working prototype 2025-11-24 20:26:11 -05:00
elk-task-compile.el working prototype 2025-11-24 20:26:11 -05:00
elk-task-test.el feat(task-test): set scope to test for built-in test task 2025-11-27 22:58:31 -05:00
elk.el implement running tasks in different emacsen 2026-01-05 09:21:36 -05:00
Elkfile implement running tasks in different emacsen 2026-01-05 09:21:36 -05:00
README.md update docs 2026-01-05 10:11:26 -05:00

elk - Emacs Lisp Task Runner

elk is a task runner for Emacs Lisp packages, inspired by Ruby's Rake and Elixir's Mix.

Philosophy

"Tasks and dependencies, that's it."

elk focuses on simplicity: define named tasks with dependencies, then run them. Everything else grows from this foundation.

Features

  • Simple API: Define tasks with elk-task macro
  • Dependency Resolution: Automatic topological sort with cycle detection
  • Extensible: Drop-in tasks from ./tasks/*.el or installed packages
  • Configurable: Built-in tasks adapt to your project via elk-set
  • CLI & Interactive: Run from shell (elk compile) or Emacs (M-x elk)

Installation

git clone https://github.com/yourusername/elk.git
cd elk
chmod +x elk

# Add to PATH or create symlink
sudo ln -s $(pwd)/elk /usr/local/bin/elk

Quick Start

Built-in Tasks

elk compile    # Byte-compile .el files
elk test       # Run ERT tests
elk clean      # Remove .elc files

Define Custom Tasks

Create ./tasks/deploy.el:

(elk-task deploy
  "Deploy to MELPA"
  :depends (compile test)
  :action (lambda ()
            (message "Deploying to MELPA...")
            ;; deployment logic here
            ))

Run it:

elk deploy  # Runs compile -> test -> deploy

Task with Arguments

(elk-task test
  "Run tests with optional pattern"
  :args (pattern)
  :action (lambda (&rest args)
            (let ((pattern (plist-get args :pattern)))
              (ert-run-tests-batch (or pattern t)))))
elk test --pattern=my-test-*

Project Configuration

Create an optional Elkfile at project root:

;;; Elkfile --- elk configuration

(elk-project
 :name "my-package"
 :version "1.0.0"
 :source-dirs '("lisp/")
 :test-dirs '("test/"))

;; Configure built-in tasks
(elk-set 'test-framework 'ert)      ; or 'buttercup
(elk-set 'clean-patterns '("*.elc" "*.eln"))

;;; Elkfile ends here

Task API

(elk-task TASKNAME
  "Description"
  [:depends (TASK1 TASK2 ...)]
  [:args (ARG1 ARG2 ...)]
  :action (lambda (&rest args) ...))
  • TASKNAME: Symbol, task name (e.g., compile, buttercup:run)
  • :depends: Optional list of task symbols to run first
  • :args: Optional list of argument names (passed as --name=value on CLI)
  • :action: Lambda to execute

Task Discovery

elk discovers tasks from three sources (in order):

  1. Built-in: elk:compile, elk:test, elk:clean
  2. Packages: Files named <package>-elk-task-*.el on load-path
  3. Local: ./tasks/*.el in project directory

Later sources override earlier ones with same name.

Namespacing

Package tasks use : namespacing:

;;; buttercup-elk-task-run.el
(elk-task buttercup:run
  "Run Buttercup specs"
  :action (lambda () (buttercup-run-discover)))
elk buttercup:run

Interactive Use

Within Emacs:

M-x elk RET            ; Choose task with completion
M-x elk-list-tasks     ; List all available tasks
(elk-run 'compile)     ; Run programmatically

CLI Interface

elk TASK [--arg=value ...]  # Run task
elk --tasks                 # List available tasks (NYI)
elk --help                  # Show help (NYI)
elk --dry-run TASK          # Show execution order (NYI)

Multi-Version Testing

Run tasks on different Emacs versions using containers:

# Run tests on Emacs 28.2
elk test --emacs=28.2

# Run compile on Emacs 29.4
elk compile --emacs=29.4

From Elisp:

(elk-run 'test :emacs "28.2")

Requirements

Configuration

Override container runtime in Elkfile:

(elk-set 'container-runtime 'docker)  ; or 'podman (default: auto-detect)

How It Works

  • First run pulls the container image (may take a minute)
  • Dependencies install into .elk/VERSION/deps/ (isolated per version)
  • Output streams to *elk-VERSION* buffer (interactive) or stdout (CLI)

Extending elk with Middleware

Intercept and modify task execution without touching elk's core. Add logging, routing, timing, caching—anything that wraps around task execution.

For plugin authors and power users who want elk to do more than run tasks.

Your First Middleware in 30 Seconds

Add this to your Elkfile:

(elk-add-middleware
 (lambda (task args next-fn)
   (message ">>> Starting %s" task)
   (funcall next-fn task args)
   (message "<<< Finished %s" task)))

Run any task:

elk clean
# >>> Starting clean
# elk: Running clean...
# elk: clean completed
# <<< Finished clean

You just wrapped every task with logging. The next-fn call continues to the actual task.

How Middleware Works

Think of middleware like layers of an onion. Each layer wraps around the core:

[your-middleware] → [container-middleware] → [elk--run-core]

When you run elk compile:

  1. Your middleware runs first
  2. It calls next-fn, which invokes the next layer
  3. Eventually elk--run-core runs the actual task
  4. Control returns back through each layer

Each middleware decides whether to:

  • Continue: Call next-fn to proceed to the next layer
  • Short-circuit: Skip next-fn to intercept entirely (like container routing)
  • Modify: Change task or args before passing them on

Real Example: How Container Support Works

The --emacs=VERSION flag isn't magic—it's middleware. Here's the pattern:

(defun elk-container--middleware (task-name args next-fn)
  (let ((emacs-version (plist-get args :emacs)))
    (if emacs-version
        ;; Intercept: run in container instead
        (elk-container--run task-name emacs-version args)
      ;; Continue: no version specified, proceed normally
      (funcall next-fn task-name args))))

(elk-add-middleware #'elk-container--middleware)

When you run elk test --emacs=28.2:

  1. Container middleware sees :emacs "28.2" in args
  2. It routes to a container instead of calling next-fn
  3. The task runs in Docker/Podman, not locally

When you run elk test (no --emacs):

  1. Container middleware sees no :emacs
  2. It calls next-fn to continue normally
  3. The task runs locally

Common Patterns

Timing tasks:

(elk-add-middleware
 (lambda (task args next-fn)
   (let ((start (float-time)))
     (funcall next-fn task args)
     (message "Task %s took %.2fs" task (- (float-time) start)))))

Conditional execution:

(elk-add-middleware
 (lambda (task args next-fn)
   (if (and (eq task 'deploy) (not (getenv "CI")))
       (message "Deploy only runs in CI")
     (funcall next-fn task args))))

Modifying arguments:

(elk-add-middleware
 (lambda (task args next-fn)
   ;; Always run tests in verbose mode
   (when (eq task 'test)
     (setq args (plist-put args :verbose t)))
   (funcall next-fn task args)))

Middleware API Reference

Function Description
elk-add-middleware FN Register middleware function

Middleware function signature:

(lambda (task-name args next-fn) ...)
Parameter Type Description
task-name symbol Task being run (e.g., 'compile)
args plist Arguments passed to task (e.g., (:pattern "foo"))
next-fn function Call to continue the middleware chain

Execution order: First registered = outermost (runs first, returns last).

Self-registering plugins: Put (elk-add-middleware #'your-fn) at the end of your .el file. When it's loaded, the middleware registers automatically.

Architecture

elk.el                  # Main entry point
├── elk-core.el         # Task struct, registry, elk-task macro
├── elk-runner.el       # Dependency resolution, execution, middleware
├── elk-cli.el          # Command-line argument parsing
├── elk-discover.el     # Project detection, Elkfile loading
├── elk-deps.el         # Dependency management
├── elk-container.el    # Multi-version container support
└── elk-task-*.el       # Built-in tasks

Examples

Task with Dependencies

(elk-task package
  "Create distributable .tar"
  :depends (clean compile test)
  :action (lambda ()
            (package-upload-file "my-package.tar")))

Execution order: cleancompiletestpackage

Configurable Test Framework

;; In Elkfile
(elk-set 'test-framework 'buttercup)

The built-in test task adapts automatically.

Override Built-in Task

;;; tasks/compile.el
(elk-task compile
  "Custom compile with linting"
  :action (lambda ()
            (package-lint-batch-and-exit)
            (byte-recompile-directory "." 0)))

Testing

elk uses TDD with ERT:

emacs --batch -L . -L test \
  -l test/elk-core-test.el \
  -l test/elk-runner-test.el \
  -l test/elk-cli-test.el \
  -f ert-run-tests-batch-and-exit

All tests pass with 0 unexpected.

Design Decisions

  • Pure Emacs Lisp: No external dependencies
  • Tasks, not targets: Unlike Make's file-based model
  • Symbol names: Easier to quote, cleaner syntax
  • Named flags: --pattern=value instead of positional args
  • Two-tier discovery: Local always wins over global

Roadmap

MVP complete. Future enhancements:

  • elk init - Generate starter Elkfile
  • elk --tasks - List available tasks with descriptions
  • elk --help - CLI help output
  • Parallel task execution
  • Watch mode (elk watch compile)
  • Integration with package-lint, checkdoc

Comparison

Tool Approach Extensibility
Cask Declarative Very limited
Eask Hybrid JS/Bash Medium
Eldev Config as code Excellent
elk Tasks + deps Excellent

elk aims for the sweet spot: simpler than Eldev, more extensible than Cask.

License

MIT (or your choice)

Contributing

Contributions welcome! elk uses TDD - write tests first, watch them fail, then implement.