Using Neovim as Lua interpreter with Luarocks

  Saturday, January 21, 2023 » Neovim

This is a short article covering how you can use Neovim as Lua interpreter for Luarocks and busted.

Update 2023-11-05:

See nlua for a ready-to-use version

Motivation ยถ

In Testing Neovim LSP plugins I wrote:

To run tests you need a test runner. Busted is a popular choice for Lua, but for Neovim using the test harness thatโ€™s part of plenary is currently the more convenient option. This is because Neovim embeds Lua and youโ€™ll want to run your tests using this embedded โ€œNeovim Luaโ€ to have access to the vim module. If you ran busted directly it would use your system Lua, without access to the vim module.

In the future it may become possible to run the normal busted and tell it to use Neovim as Lua interpreter, if Neovim adds support for that. (See repurpose โ€œ-lโ€ to execute Lua)

It turns out this future happened. @justinmk merged a change to Neovim that allows to use the -l command line option to run Lua scripts and pass any trailing arguments to the script.

Examples:

nvim -l foo.lua --arg1 --arg2

Or:

nvim -l <(echo "print(10)")

See :help -l for more details if you’re using Neovim nightly (Or in the future, Neovim 0.9+).

Install Luarocks ยถ

First we need to install Luarocks. I recommend using a package manager. On Archlinux I use pacman:

pacman -S luarocks

Create a nlua wrapper ยถ

Luarocks supports configuring the Lua interpreter it should use, but we can’t point it to nvim -l because of two reasons:

  1. Luarocks only allows setting the interpreter to a command that must be available in /usr/bin (at least on Linux). You can’t have an argument included in the command.
  2. The command line interface of a regular Lua interpreter is different then what nvim or nvim -l offers:
usage: lua [options] [script [args]]
Available options are:
  -e stat   execute string 'stat'
  -i        enter interactive mode after executing 'script'
  -l mod    require library 'mod' into global 'mod'
  -l g=mod  require library 'mod' into global 'g'
  -v        show version information
  -E        ignore environment variables
  -W        turn warnings on
  --        stop handling options
  -         stop handling options and execute stdin

As far as I could tell, Luarocks depends on the -e option and otherwise passes along the script with its arguments.

I created a Haskell script which I put into /usr/bin/nlua to emulate that. The script is rather simplistic and doesn’t support any other options. All it does is rewrite -e some_lua_expression into -c "lua some_lua_expression" and prefix the remainder with an -l. Here is the full script:

#!/usr/bin/env stack
{- stack script --resolver lts-20.5 -}

import System.Environment (getArgs)
import System.Process (spawnProcess, waitForProcess)
import System.Exit (exitWith)


main :: IO ()
main = do
  args <- remap <$> getArgs
  handle <- spawnProcess "v" ("-Es" : args)
  waitForProcess handle >>= exitWith


--- >>> remap ["-e", "stat"]
-- ["-c","lua stat"]
--
--- >>> remap ["-e", "stat", "luascript_path"]
-- ["-c","lua stat","-l","luascript_path"]
--
--- >>> remap ["-e", "stat", "-l", "luascript_path"]
-- ["-c","lua stat","-l","luascript_path"]
remap :: [String] -> [String]
remap [] = []
remap ("-e" : x : xs)
  | null xs = newHead
  | head xs == "-l" = newHead <> xs
  | otherwise = newHead <> ("-l" : xs)
  where
    newHead = ["-c", "lua " <> x]
remap (x : xs) = x : remap xs

(See this follow up post for a Lua version of the script)

Configure Luarocks ยถ

Next we need to configure luarocks to use the new nlua interpreter and set the right Lua version. In a shell, run:

luarocks config lua_version 5.1
luarocks config lua_interpreter nlua
luarocks config variables.LUA_INCDIR /usr/include/luajit-2.1

To check that it works, run luarocks. You should see something like this:

Configuration:
   Lua:
      Version    : 5.1
      Interpreter: /usr/bin/nlua (ok)
      LUA_DIR    : /usr (ok)
      LUA_BINDIR : /usr/bin (ok)
      LUA_INCDIR : /usr/include/luajit-2.1 (ok)
      LUA_LIBDIR : /usr/lib (ok)

Install busted ยถ

Now install busted in a local tree:

luarocks --local install busted

If the interpreter wrapper works, you should see something like this:

โฎ luarocks --local install busted
Installing https://luarocks.org/busted-2.1.1-1.src.rock

busted 2.1.1-1 depends on lua >= 5.1 (5.1-1 provided by VM)
busted 2.1.1-1 depends on lua_cliargs 3.0 (3.0-2 installed)
busted 2.1.1-1 depends on luafilesystem >= 1.5.0 (1.8.0-1 installed)
busted 2.1.1-1 depends on luasystem >= 0.2.0 (0.2.1-0 installed)
busted 2.1.1-1 depends on dkjson >= 2.1.0 (2.6-1 installed)
busted 2.1.1-1 depends on say >= 1.4-1 (1.4.1-3 installed)
busted 2.1.1-1 depends on luassert >= 1.9.0-1 (1.9.0-1 installed)
busted 2.1.1-1 depends on lua-term >= 0.1 (0.7-1 installed)
busted 2.1.1-1 depends on penlight >= 1.3.2 (1.13.1-1 installed)
busted 2.1.1-1 depends on mediator_lua >= 1.1.1 (1.1.2-0 installed)
busted 2.1.1-1 is now installed in /home/user/.luarocks (license: MIT <http://opensource.org/licenses/MIT>)

You can take a look at the busted executable that it created to see how it uses the nlua interpreter:

#!/bin/sh

LUAROCKS_SYSCONFDIR='/etc/luarocks' exec \
    '/usr/bin/nlua' \
    -e \
    'package.path="/h/u/.luarocks/share/lua/5.1/?.lua;/h/u/.luarocks/share/[....]' \
    '/h/u/.luarocks/lib/luarocks/rocks-5.1/busted/2.1.1-1/bin/busted' "$@"

Running busted ยถ

As last step we verify if busted works on tests which are using the vim module to ensure it uses the Neovim Lua. I used the nvim-lint test suite:

โ†ช  ~/.luarocks/bin/busted tests
โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—
31 successes / 0 failures / 0 errors / 0 pending : 0.022985 seconds

๐ŸŽ‰