luaguides

Dependency Management Patterns in Lua

Lua has no built-in package manager. That means dependency management is a question every Lua project eventually has to answer. The right choice depends on whether you’re writing a game plugin, a server application, or a library you’re distributing to others.

The Options

  1. Luarocks — Lua’s community package manager
  2. Git submodules: include a library as a subdirectory in your project
  3. Inline bundling: copy the library code directly into your project
  4. Manual download: fetch and place the library files yourself

Each approach has tradeoffs in reliability, portability, and maintenance burden. The right pick depends on your deployment target: a server application running in a controlled CI environment can lean on LuaRocks for automated resolution, while a game mod distributed as a single file to end users is better served by inline bundling that requires zero toolchain setup on the player’s machine.

Using Luarocks

Luarocks is the closest thing Lua has to a standard package manager. It installs packages to a user-configured path and lets you require them by name.

# Install a package
luarocks install luasocket

# Install to a specific prefix
luarocks install --local luasocket

The install command places rocks into a managed tree under your home directory or a system prefix, but Lua’s runtime does not automatically search there. You need to tell the Lua interpreter where LuaRocks deposited the modules by updating the Lua path variables. The luarocks path command prints the environment variable assignments that match your current Lua version and installation prefix, which you can eval in your shell or embed in a launcher script:

luarocks path --bin
# Adds /home/user/.luarocks/share/lua/5.1 to LUA_PATH

This output injects the rocks tree into LUA_PATH (for pure-Lua modules) and LUA_CPATH (for compiled C extensions like luasocket’s .so backend). Without these paths set, calling require("socket") would fail because Lua’s default module searchers only scan the standard library directories. With the paths active, the require function walks each entry in package.path, substitutes ? with the module name, and loads the first matching file it finds. In your code:

local socket = require("socket")

Luarocks handles versioning and dependency resolution. It understands semantic version constraints and will pull in transitive dependencies automatically, which saves you from manually chasing down a chain of library requirements. The catch: you need luarocks installed on every machine that runs your project, and not every library is available as a rock. For Lua modules that wrap C libraries, you also need a working C compiler toolchain on the target system.

Git Submodules

If you need a specific version of a library and want it tracked in your repo, git submodules work well:

# Add a library as a submodule
git submodule add https://github.com/example/lua-library.git vendor/library

The submodule checkout places the library files at a known relative path inside your repository, but Lua’s default module loader has no awareness of this location. You must teach require to traverse the vendor directory by extending package.path with a pattern that matches the module name. The semicolon before ./vendor/library/?.lua appends the new search entry after the built-in paths, preserving the standard library lookup order while adding your vendored code as a fallback:

package.path = package.path .. ";./vendor/library/?.lua"
local lib = require("library")

Submodules pin you to a specific commit, which is good for reproducibility. But they add friction to updates. Bumping a dependency means opening a separate PR in the submodule repo, merging it, then updating the parent repo’s submodule pointer. Collaborators who clone without --recursive will find an empty vendor directory and broken builds.

Inline Bundling

For distribution or self-contained projects, many developers copy dependencies into a lib/ directory:

myproject/
  lib/
    sockets.lua
    lpeg.lua
  main.lua

This flat directory layout maps directly to how Lua’s require function resolves module names to filesystem paths. When you call require("sockets"), Lua replaces the ? in each package.path entry with the module name, producing a candidate like ./lib/sockets.lua. If the file exists, Lua loads and executes it, returning whatever the module file returns. Adding the lib directory to the search path makes this resolution work:

package.path = package.path .. ";./lib/?.lua"
local socket = require("sockets")

The advantage: everything lives in one repo, no external dependencies. The downside: updating means manually replacing files, and you lose version history for the library.

Inline bundling is common in Roblox plugins and game scripts where external package managers aren’t available. Roblox in particular runs Lua in a sandboxed environment with no shell access, so luarocks or git are not options — inline bundling is the only practical way to ship reusable code. Some Roblox developers maintain a shared ModuleScript library folder inside their place file and use a build script on their development machine to copy dependencies in before publishing.

Writing your own require handler

For more control, you can implement a custom module loader:

local function add_search_path(path)
  package.path = package.path .. ";" .. path .. "/?.lua"
  package.path = package.path .. ";" .. path .. "/?/init.lua"
end

-- Add project-local lib directory
add_search_path("./lib")

-- Add vendor directory
add_search_path("./vendor")

Manually extending package.path gives you fine-grained control over load order and lets you handle missing modules gracefully. By programmatically managing package.path instead of hardcoding it, your application can adapt to different deployment layouts. For example, searching a development lib/ directory during testing and a system-wide /usr/share/lua/ path in production. The same loader infrastructure also enables conditional dependency loading, where you probe for optional libraries before deciding how to proceed.

Detecting installed packages

Before requiring an optional dependency, check if it’s available:

local function package_available(name)
  local ok, err = pcall(require, name)
  return ok
end

if package_available("luasocket") then
  local socket = require("socket")
  -- use socket
else
  -- fallback or error
end

Many libraries use this pattern to support optional dependencies without forcing them on users. The pcall wrapper catches the error that require throws when a module is absent, turning a hard crash into a boolean decision. This is especially useful for libraries distributed through LuaRocks, where users may install your package but skip an optional backend or driver that they don’t need for their use case.

Working with OpenResty and LuaRocks

OpenResty has its own module path hierarchy:

-- OpenResty loads from these paths automatically
package.path = package.path .. ";/usr/local/openresty/lua/?.lua"

OpenResty embeds LuaJIT rather than standard PUC-Rio Lua, and its module tree is rooted at a fixed system prefix. When you install rocks for an OpenResty application, you must target the LuaJIT ABI and the OpenResty-specific paths so that compiled C modules link against the correct Lua headers and land in a directory the nginx worker processes can read. If you need a rock in OpenResty, install it to the OpenResty path:

luarocks --lua-version=5.1 install luasocket

The --lua-version flag is per-invocation, which works for one-off installs but becomes tedious when you manage multiple rocks for the same OpenResty deployment. A more maintainable approach is to set the LuaRocks configuration persistently so every subsequent luarocks install command automatically writes to the correct tree. Or configure the path explicitly:

luarocks configlua_path /usr/local/openresty/lua/?.lua

vendoring Libraries

LuaRocks solves installation but not deployment. Your production server or game runtime may not have LuaRocks available at all. Vendoring libraries sidesteps this by copying dependency source files directly into your project tree, so everything ships as a self-contained unit. A common pattern is to vendor libraries: copy them into your project and load them from a local path:

-- lib/mylib.lua
local mylib = {}

function mylib.foo()
  return "foo"
end

return mylib

The library file follows Lua’s standard module contract: it creates a local table, populates it with functions, and returns the table at the end of the file. When require("mylib") runs, Lua executes this file in a protected environment and captures the return value, caching it in package.loaded so subsequent requires return the same table without re-executing the source. The main script then wires everything together:

-- main.lua
package.path = package.path .. ";./lib/?.lua"
local mylib = require("mylib")

Vendoring gives you a stable, reproducible copy that works without any package manager installed. This is the strategy used by many Lua game engines and embedded Lua hosts where the runtime environment is fixed and you control the entire module tree. The trade-off is that you assume responsibility for tracking upstream changes and applying security patches yourself, since there is no luarocks update equivalent for vendored code.

When to use what

ApproachBest for
LuarocksServer apps, CI environments, libraries you distribute
Git submodulesProjects where you want version-pinned dependencies
Inline bundlingGames, plugins, single-file distributions
Custom requireWhen you need specific load order or fallbacks

Many real-world Lua projects combine several of these strategies. A typical setup might use LuaRocks for core libraries during development, vendor a patched fork of a third-party module that isn’t published as a rock, and ship the final application with all dependencies bundled into a single directory for deployment.

See Also