Create, build, and publish modules for Lua
Lua rocks!
The first article was covering the basics of managing a project and adding dependencies with LuaRocks, as well as having a how-to for installing Lua and LuaRocks.
This article will focus on how to create, build, and publish a Lua package with documentation and version management. If you just want a quick summary, you can jump down to the TL;DR at the end of the page.
Prerequisites
At least Lua and LuaRocks will be required for this article to follow along. The first article of this series shows how to install both for macOS, Linux, and Windows.
Create a new package
To create a new package, the LuaRocks init command can
be used. It is a convenient way of setting up a project using
locally installed dependencies. The init command will
create some reasonable defaults, but it is recommended to
at least specify the supported Lua versions.
If not defined, the project name will be taken from the folder name
the command is called from, the default version will be
dev-1. Providing at least those three options —
the supported Lua versions, a project name, and a project start
version — the command to initialize a package will look like
the following.
$ luarocks init \
--lua-versions "5.1,5.2,5.3,5.4,5.5" \
my-package 1.0.0
$ is used to show a command will be entered.
This will create a .luarocks folder, mainly for config
files, and the lua_modules folder containing locally
installed dependencies. The presence of both of
those folders marks the root of the project
directory.
Two executables are created as well, ./lua and
./luarocks, offering a way to run both commands for the
local project, configured for the local dependency tree, and a
default .gitignore file, containing both folders and
the two local commands. If a .gitignore file is already
present, the four entries will be appended to it.
One file not to be ignored is the generated
.rockspec file. It holistically describes a package and
how it is handled.
Short note about the project structure
With the files and folders predefined through LuaRocks, I
personally like to have all the code I write in a
src (source) folder, including tests. I do not use a
dedicated test folder, as I keep tests close to the source files*.
Another folder in the project root is the doc folder
used for documentation.
Any build artifacts that will be created, like
.rock archive files by running
luarocks pack, will be ignored through the
.gitignore file.
/*.rock
.rock files.Rockspec deep-dive
The .rockspec is a Lua file, though without access to
any Lua functions. When editing the file it is good practice to
check that it is well-formed by running the LuaRocks
lint command.
$ ./luarocks lint
.rockspec file is well
written.
Even at the cost of repeating the official documentation, I think it is worth looking at the spec file in more detail — specifically the general metadata, dependencies, and package source rules.
Package metadata
The project metadata is the basis of the package description. There
are only
two mandatory fields for that section,
package being the package name, and
version. Though being able to fill in more is better,
providing solid help for package users — even if that is
future me.
Another crucial field is the rockspec_format, with many
packages using the oldest format 1.0. To get the most
out of a .rockspec file, version 3.0 is
recommended, giving the greatest range of options to describe a
package.
Starting with those few fields, let's create a comprehensive
.rockspec file.
-- my-package-1.0.0-1.rockspec
rockspec_format = "3.0"
package = "my-package"
version = "1.0.0-1"
Package source
When creating a package, it is also mandatory to define how to
actually retrieve the contents on build and/or install. This is done
through the
source.url
property, taking a multitude of options, from different source
control systems like Git or Mercurial to archive files and even
FTP. All options can be found in the
official LuaRocks documentation about build rules.
No matter the protocol or archive type used, important is that
the source needs to be available for the used context. That means that if the package is published as an archive to the
official LuaRocks registry, the URL needs to point to a publicly
available source. If the package is only used locally, for example,
during development, the file:// protocol can be used,
utilizing the local file system.
A very common use case is
pointing to a Git repository combined with a
specific branch or release tag. Assuming that releases are done via
version tags, e.g., version 1.1.0 being the Git tag
v1.1.0, this is how the configuration for this looks.
-- my-package-1.0.0-1.rockspec
-- earlier options ...
source = {
url = "git+https://github.com/MartinHelmut/lua-series",
tag = "v1.1.0"
}
Using a specific branch works similar.
source = {
url = "https://github.com/MartinHelmut/lua-series.git",
-- This is also the branch name for this article
-- in the companion repository.
branch = "part-2"
}
Supported platforms
In case where the package is only supported on a few platforms, the
supported_platforms property can be used to signal
this. It's an array of strings with the following
full list of platform names: "unix",
"bsd", "solaris", "netbsd",
"openbsd", "freebsd", "dragonfly",
"linux", "macosx", "cygwin",
"msys", "haiku", "windows",
"win32", "mingw", "mingw32",
"msys2_mingw_w64".
With this, a platform can be excluded that is not supported by using
the
! as prefix before the platform name.
supported_platforms = {
"!macosx"
}
Alternatively, a list of explicitly supported platforms can be given.
supported_platforms = {
"linux",
"macosx"
}
Package descriptions
The next larger chunk is the description table,
containing a short and long description, the package license, links
and maintainer information, and the option to add categories or
labels to the package.
-- my-package-1.0.0-1.rockspec
-- earlier options ...
description = {
summary = "This is a Lua series package.",
detailed = "This package is for educational purposes ...",
license = "MIT",
homepage = "https://github.com/MartinHelmut/lua-series",
issues_url
= "https://github.com/MartinHelmut/lua-series/issues",
maintainer
= "Martin Helmut Fieber <[email protected]>",
labels = { "lua-series", "educational" }
}
description. The MIT-license is the same used
by Lua >= 5.
If the description.homepage field is defined, a package
user can open the homepage through LuaRocks by using the
doc command.
$ ./luarocks doc --home
Dependencies
Declaring dependencies in the project, no matter what kind, is done via a list of strings containing the dependency name and what version it uses. Those names will be looked up in the LuaRocks package registry on installation.
Versions can be specified in a variety of ways, all of which are
documented in the official
LuaRocks documentation about dependencies. The most common way to specify versions is via the operators
==, ~=, <, >,
<=, and >=; and via a comma
, separated range.
{
-- The first two are the same, using a
-- specific version.
"package1 1.0",
"package1 == 1.0",
-- A version smaller 2.
"package2 < 2",
-- Multiple, exact version 2.1 and greater
-- or equal 2.3, skipping 2.2.
"package3 2.1, >= 2.3",
}
Let's look at the three properties most often used to
declare dependencies, starting with
dependencies. This is what you generally use to declare
what dependencies the project will include and depend on. Having run
the init command, this will already contain a special
dependency, Lua, defining what versions of Lua the package supports.
dependencies = {
"lua >= 5.1, < 5.6"
}
When setting up a project, all dependencies in that list can be installed via the LuaRocks install command.
$ ./luarocks install --deps-only my-package-1.0.0-1.rockspec
.rockspec.
There are also build_dependencies and
test_dependencies, used for dependencies at build and
test time, respectively. Both work the same as
dependencies, but are not installed via the above
command. The build_dependencies are only installed when
running luarocks build, the
test_dependencies when running
luarocks test.
build_dependencies = {
"ldoc >= 1.4"
}
test_dependencies = {
"luaunit >= 3.4"
}
luarocks build, and
LuaUnit
on luarocks test.
The external_dependencies field is specifically used to
tell LuaRocks where to find, e.g., a C-library. I will go into
detail on how to use this in a later article about C-rocks.
Platform specific dependencies
A lot of the .rockspec properties support
platform-specific options, and dependencies are no exception. With
this, it is possible to install specific packages or package
versions only on certain systems. A list of all supported platform
names can be found
earlier in this article under Supported Platforms.
dependencies = {
"lua >= 5.1, < 5.6",
"luaunit 3.2",
platforms = {
windows = { "luaunit 3.3" },
unix = { "luaunit >= 3.4" }
}
}
Install from git or url
With LuaRocks, it is also possible to install packages from source
control or a URL directly. The way this works is by
pointing to a remote .rockspec file, for example, in a
Git repository, as seen in a
later example.
$ ./luarocks install \
https://raw.github.com/MartinHelmut/lua-series/v1.0.0/lua-series-1.0.0-1.rockspec
Be aware, dependencies installed like this
can not be added to dependencies,
build_dependencies, or
test_dependencies in the projects
.rockspec file.
Cross-server dependency tracking
If it is planned to install dependencies from other registries than the default LuaRocks, a LuaRocks config file can be used to define multiple "rock servers".
Thanks to the local setup, this can be done per project inside the
.luarocks/config.lua file. Usually this file is
generated with a specific version, like
.luarocks/config-5.5.lua, when using Lua 5.5.
rocks_servers = {
"https://luarocks.org/repositories/rocks"
}
Documentation
LuaRocks supports bundling documentation about the package inside
the package itself. The idea is to use
build.copy_directories in the
.rockspec file to include a doc folder.
The folder name is special, as it will be the one
used when calling luarocks doc.
-- my-package-1.0.0-1.rockspec
-- other options ...
build = {
copy_directories = { "doc" }
}
doc folder on
luarocks build.
With this setup, calling LuaRocks' doc command for the
package will list the contained documentation files; in this
example, one Markdown file I created.
$ ./luarocks doc my-package
Documentation files for my-package 1.0.0-1
------------------------------------------
/Users/You/Projects/my-package/1.0.0-1/doc/
Usage.md
The doc command has two more options; one is calling it
with --home to open the project home page defined
through description.homepage in the default browser.
$ ./luarocks doc my-package --home
The other uses the --porcelain flag to get a
machine-readable output of the list of contained documentation
files.
$ ./luarocks doc my-package --porcelain
/Users/You/Projects/my-package/1.0.0-1/doc/Usage.md
Pure vs. Source vs. Binary
When packing a project for distribution, there are three types of resulting archives that are referred to: pure-, source-, and binary-rock.
Pure-rock
The so-called pure-rock is a package containing
only Lua files. There is no need to compile
anything on installation, and the package is most often
platform-independent. Though usage can be restricted through the
.rockspec by using the
supported_platforms property.
Source-rock
A source-rock is a package that probably needs to be compiled on installation. It can contain a variety of files, build steps, and even platform-dependent code. This is a common package type to find.
Binary-rock
At last, the binary-rock, most often containing platform-specific compiled C-code but also containing Lua files. This package type also contains a manifest file, defining MD5 checksums for all contained executables.
Why?
One may ask, why distinguish between those types of packages? It is important to keep in mind what happens when distributing a package. Being able to skip a build step with a pure-rock can be desirable because it avoids adding specific code per platform.
Other times, targeting specific platforms is a given, for example when using C-modules, so it needs to be clear how a package is installed on the target system. Those things are important for pure- and source-rocks.
Binary-rocks have some other requirements that set them apart, most notably containing at least one executable and a manifest file.
Building a module
Knowing what rock types exist, building a binary-rock will not be part of this article, and a later article in the series will cover C-rocks specifically.
To build a project written in Lua, LuaRocks needs to know where all
Lua modules are located and how to build them. The
"built-in" build type will suffice. To configure this, the
.rockspec file will get a build table.
-- my-package-1.0.0-1.rockspec
-- earlier options ...
build = {
type = "builtin",
modules = {}
}
Inside build.modules all available modules need to be
listed. Let's say there is a src/main.lua file that
should be the packages' entry point. And a
src/my-package/utils.lua as a submodule.
-- my-package-1.0.0-1.rockspec
-- earlier options ...
build = {
type = "builtin",
modules = {
["my-package"] = "src/main.lua",
["my-package.utils"] = "src/my-package/utils.lua"
}
}
Alternatively, if a folder named lua is used in the
package root, all containing files will be considered for build, as
if every file had an entry in build.modules.
With the setup done, the project can be built.
$ ./luarocks make
luarocks build can also be used. The
difference is that make
does not fetch any sources.
Publish your rock
Usually there are two ways to publish a package for LuaRocks — one is where you provide a packaged source, for example, built from a Git or Mercurial repository; the second option is publishing the code via an archive, like ZIP or Tarball, and having a URL point to it.
LuaRocks supports Git, CVS, Subversion, and Mercurial. I will show as an example the usage via a Git repository, as the usage for the other source control management systems is similar.
Using a code repository
When using a Git repository, it is important to
use a tagged version; otherwise, the package is
pointing to the latest point in development. If this is the desired
behavior the .rockspec should be versioned as
SCM (Source Code Management), resulting in a file name
like my-package-scm-1.rockspec.
In any other case, a proper version tag should be used, for example
v1.0.0 for the first stable version of a package
(assuming the usage of
semantic versioning). As shown in the section about
Package source, defining Git as a
source looks as follows.
source = {
url = "git+https://github.com/MartinHelmut/lua-series",
tag = "v1.0.0"
}
To complete the example, here is how to create and push a Git tag.
$ git tag v1.0.0 && git push --tags
When eventually packing and submitting the rock, this will be used to pack the sources so that users won't need the respective source control system installed. Though it is advised to use a stable URL nonetheless.
Using an archive
Archives use ZIP or Tarball, containing all sources in a folder with the same name and version. The only important structure to keep is the top-level package name with the version in the archive; everything else can be customized to the package structure, containing sources, documentation, tests, and so on.
$ tar czvpf my-package-1.0.0.tar.gz my-package-1.0.0/
Counterintuitive for me, the archive needs then to be made
available online, on a stable URL*, and linked from
the .rockspec sources.
source = {
url = "https://stable.tld/my-package/my-package-1.0.0.tar.gz"
}
Build your rock
Before packing the project for upload, it needs to be built. The
build command will build the rock and install it into
the project folder as a dependency. When only one
.rockspec is present, no arguments are needed.
$ ./luarocks build
By default, this will build the package provided by the
.rockspec file. In the case of a Git repository, it
will be cloned locally and built from there. If
this is not desired, a local-only build can be created via the flag
--pack-binary-rock.
$ ./luarocks build --pack-binary-rock
Ship it!
After the project is built, to actually publish the rock, an account
at
luarocks.org
is needed. From the
account settings page, a new API key needs to be created, enabling the
usage of the
luarocks upload
command.
What is left is packing the "rock" for publish.
$ ./luarocks pack my-package-1.0.0-1.rockspec
After packing the module, it is now possible to publish the package for inclusion in the official LuaRocks registry.
$ ./luarocks upload my-package-1.1.0-1.rockspec \
--api-key=<API_KEY_HERE>
The successful published package can then be found on LuaRocks.
Version management
Create a new version
After making changes to a project,
creating a new version
can be done via luarocks new_version. Running this
command will create a new .rockspec file with the
updated version number.
-- my-package-1.1.0-1.rockspec
rockspec_format = "3.0"
package = "my-package"
version = "1.1.0-1"
source = {
url = "git+https://github.com/MartinHelmut/lua-series",
tag = "v1.1.0"
}
.rockspec looks. Both the
version and the source.tag are set.
Remark: This will not create a new Git tag. The
newly created
.rockspec
file needs to be committed first, and a new Git tag needs to be
manually added afterward.
With the new .rockspec file, and a pushed Git tag, the
latest version can be published by using the pack and
upload command.
$ ./luarocks pack my-package-1.1.0-1.rockspec
$ ./luarocks upload my-package-1.1.0-1.rockspec \
--api-key=<API_KEY_HERE>
Old package versions
What about the old version? It is a common practice to keep older
.rockspec version files around inside a
.rockspec folder in the project.
I do not do this — it is always possible to
point to older versions with source control management systems, so I
just remove the old .rockspec file.
For example, pointing to a specific tag with a package on GitHub using the raw.github.com path name.
$ ./luarocks install \
https://raw.github.com/MartinHelmut/lua-series/v1.0.0/lua-series-1.0.0-1.rockspec
TL;DR
Alright, here is a boiled-down version of creating and publishing a new package to the official LuaRocks registry using GitHub.
Create a new package.
$ luarocks init \
--lua-versions "5.1,5.2,5.3,5.4,5.5" \
my-package 1.0.0
Write your base .rockspec file.
-- my-package-1.0.0-1.rockspec
rockspec_format = "3.0"
package = "my-package"
version = "1.0.0-1"
source = {
url = "git+https://github.com/YourName/my-package",
tag = "v1.0.0"
}
description = {
summary = "Amazing Lua package.",
detailed = [[
A long description of this amazing
Lua package.
]],
homepage = "https://github.com/YourName/my-package",
license = "MIT",
issues_url = "https://github.com/YourName/my-package/issues",
labels = {
"my-package",
"another-label"
},
maintainer = "Your Name <[email protected]>"
}
dependencies = {
"lua >= 5.1, < 5.6",
"good-package >= 1"
}
build = {
type = "builtin",
modules = {
main = "src/main.lua"
},
copy_directories = { "doc" }
}
test_dependencies = { "luaunit >= 3.4" }
Have a quick look to see if you did that right.
$ ./luarocks lint my-package-1.0.0-1.rockspec
.rockspec file.
Create your project and commit your code to GitHub. Tag it!
$ git tag v1.0.0 && git push --tags
Get one of those sweet, sweet API keys; build, pack, and upload your package.
$ ./luarocks build my-package-1.0.0-1.rockspec
$ ./luarocks pack my-package-1.0.0-1.rockspec
$ ./luarocks upload my-package-1.0.0-1.rockspec \
--api-key=<API_KEY_HERE>
Profit! 🎉
What comes next
The published package shown in this article can be found in the companion repository, branch "part-2" on GitHub.
The next article will be a deep dive into testing Lua code — the first small iterations, unit testing, creating mocks, and running continuous integration.
Until then 👋🏻