RPM-LUA(7)

πŸŽ€ NAME πŸŽ€

rpm-lua β€” the RPM embedded Lua interpreter. yeah bestie, RPM literally ships a whole Lua interpreter inside itself, and this page is the tour πŸ’–πŸ


✨ SYNOPSIS ✨

%scriptlet -p <lua>

%{lua:...}

🌸 DESCRIPTION 🌸

Lua is a general purpose programming language that was literally designed to be embedded inside other programs, and RPM has one of those embedded Lua interpreters baked right in so you can use it in fancy rpm-macros(7) and in transaction scriptlets. iconic behavior honestly.

Why do you care? Because the embedded Lua interpreter makes a bunch of stuff possible that is lowkey a nightmare (or just straight up impossible) with plain macros or external shell scripts. The headline one: it can help you eliminate dependency loops from package scriptlets. slay. πŸ’…


πŸ”₯ MACROS πŸ”₯

🫢 Accessing macros

The rpm extension has a bunch of functions for messing with macros, but the most convenient way to poke at rpm-macroproc(7) from the RPM Lua environment is through the global macros table. it's giving convenience, and we love that.

Lua doesn't care about the difference between table index syntax and field name syntax, so macros.foo and macros['foo'] are the exact same thing. use whichever one suits your vibe.

Like any real Lua table, items that don't exist come back as nil, and you can assign to define or undefine macros.

Example:

if not macros.yours then
    macros.my = 'my macro'
end

local v = { '_libdir', '_bindir', '_xbindir' }
for _, v in ipairs(v) do
    if not macros[v] then
        macros[v] = 'default'
    end
end

Heads up: all macros share the same global Lua execution environment. no isolated little pockets here.

πŸ’œ Calling parametric macros

Parametric macros (and that includes every built-in macro, no exceptions) can be called in a natural Lua way through the macros table, using either macros.NAME() or macros[NAME]() syntax.

Arguments get passed through a single argument, which is one of these two things:

  • a single string β€” in which case it's expanded and split with the macro-native rules
  • a table β€” in which case the table contents are used as literal arguments that are not expanded in any way

Example 1:

macros.with('foo')

Example 2:

macros.dostuff({'one', 'two', 'three'})

🌈 Returning data

By definition, anything you print() in Lua ends up in the macro expansion. but bestie, Lua macros can also return their output, which makes writing helper macros feel way more natural.

Example:

%sum() %{lua:
   local v = 0
   for _, a in ipairs(arg) do
       v = v + tonumber(a)
   end
   return v
}

πŸ¦‹ Options and arguments

Parametric Lua macros receive their options and arguments as two local tables, opt and arg. opt holds the processed option values keyed by the option character, and arg holds the arguments, numerically indexed.

These tables are always present whether or not options or arguments were actually passed β€” purely to simplify use. so you never have to vibes-check whether they exist. πŸ’…

Example:

%foo(a:b) %{lua:
if opt.b then
   print('do b')
else
   print('or not')
end
if opt.a == 's' then
   print('do s')
end
if #arg == 0 then
   print('no arguments :(')
else
   for i = 1, #arg do
      print(arg[i])
   end
end
}

🌟 SCRIPTLETS 🌟

The internal Lua can be used as the interpreter for any RPM transaction scriptlet β€” including triggers and file triggers. any of them. all of them.

Example:

%pre -p <lua>
print('Hello from Lua')

Now, the venerable /bin/sh is usually more convenient for packaging-related scripting activities (no shade, sh is fine), but the embedded Lua interpreter has some genuinely unique advantages for transaction scriptlets: they add no extra dependencies to the packages, and so they can help eliminate dependency loops. In the early "bootstrap" package set of an initial install, that can be a crucial difference. main character energy for real.

In particular, an embedded Lua script is the only generally usable option in %pretrans scriptlets during the initial installation of a system. remember that.

Embedded Lua is also much faster than executing a potentially heavyweight interpreter just to run a couple of lines of shell script. speed, bestie. πŸ”₯

Note: scriptlets using the internal Lua should not make assumptions about sharing the execution environment with other scriptlets.

πŸ“ Arguments

Scriptlet arguments are accessible from a global arg table.

Note: in Lua, indexes customarily start at 1 (one) instead of 0 (zero), and for better or for worse, the RPM implementation follows this practise. So the scriptlet arg indexes are off by one from the general expectation based on traditional scriptlet arguments. The argument containing the number of installed package instances is arg[2], and the similar argument for trigger targets is arg[3] β€” versus the traditional $1 and $2 in shell scripts. yes this will trip you up at least once. 😭

Example:

%postun -p <lua>
if arg[2] == 0  then
    print("erasing")
end

🧚 Relocatable packages

Scriptlets of relocatable packages additionally carry a global RPM_INSTALL_PREFIX table containing all the possible prefixes of the package.

Added: 4.18.0

⭐ Exit status

While scriptlets shouldn't be allowed to fail normally, you can signal scriptlet failure status if you need to, by using Lua's error(MSG, [LEVEL]) function.

πŸ‘‘ File triggers

Other file triggers receive the triggering paths via standard input, but Lua scriptlets need to call the rpm.next_line() function in a loop instead. heads up: this function only exists inside the file trigger running environment.


πŸ’– SPEC FILES πŸ’–

When you're parsing a rpm-spec(5) file with rpmbuild(1) or rpmspec(1), the RPM Lua environment contains these spec-specific global tables:

  • patches β€” the filenames of the patches in the spec, in the order they appeared in the spec.
  • patch_nums β€” the numbers of the patches in the spec, in the order they appeared in the spec.
  • sources β€” the filenames of the sources in the spec, in the order they appeared in the spec.
  • source_nums β€” the numbers of the sources in the spec, in the order they appeared in the spec.

Example:

for i, p in ipairs(patches) do
    print(string.format("echo %d: %sn", patch_nums[i], patches[i]))
end

πŸ¦‹ EXTENSIONS πŸ¦‹

On top of the Lua standard libraries (subject to whichever Lua version RPM is linked to, iykyk), the following extensions are available in the RPM internal Lua interpreter. These can be used in every context where the internal Lua can be used.

πŸ’… rpm extension

Here's the stack of RPM-specific functions available to you:

b64decode(ARG)

Perform base64 decoding on the argument. See also b64encode().

Example:

blob = 'binary data'
print(blob)
e = rpm.b64encode(blob)
print(e)
d = rpm.b64decode(e)
print(d)

b64encode(ARG [, LINELEN])

Perform base64 encoding on the argument. Line length may optionally be specified via the second argument. See also b64decode().

define("NAME BODY")

Define a global macro NAME with BODY. See also MACROS.

Example:

rpm.define('foo 1')

execute(PATH [, ARG1 [,...])

Execute an external command. This is handy for running external helper commands without depending on the shell. PATH is the command to execute, followed by an optional number of arguments to pass to the command.

For better control over the process execution and output, see rpm.spawn().

Added: 4.15.0

Example:

rpm.execute('ls', '-l', '/')

expand(ARG)

Perform RPM macro expansion on the ARG string. See also MACROS.

Example:

rpm.expand('%{_libdir}/mydir')

glob(PATTERN, [FLAGS])

Return a table of pathnames matching PATTERN. If FLAGS contains c, return PATTERN in case of no matches.

Example:

for i, p in ipairs(rpm.glob('*')) do
    print(p)
end

interactive()

Launch an interactive session for testing and debugging. Use rpmlua(1) instead.

Example:

rpm --eval "%{lua: rpm.interactive()}"

isdefined(NAME)

Test whether a macro NAME is defined and whether it's parametric, returned in two booleans. See also MACROS. (Added: 4.17.0)

Example:

if rpm.isdefined('_libdir') then
    ...
end

load(PATH)

Load a macro file from the given path. Same as the built-in %{load:...} macro.

Example:

rpm.load('my.macros')

open(PATH, [MODE[.FLAGS]])

Open a file stream using RPM IO facilities, with support for transparent compression and decompression. bestie this is the good stuff ✨

PATH is a filename string, optionally followed with a MODE string to specify open behavior:

  • a: open for append
  • w: open for writing, truncate
  • r: open for reading (default)
  • +: open for reading and writing
  • x: fail if file exists

and optionally followed by rpm-payloadflags(7) for compression and decompression.

Added: 4.17.0

Example:

f = rpm.open('some.txt.gz', 'r.gzdio')
print(f:read())

The returned rpm.fd object has the following methods:

  • fd:close() β€” Close the file stream.

Example:

f = rpm.open('file')
f:close()
  • fd:flush() β€” Flush the file stream.

Example:

f = rpm.open('file', 'w')
f:write('foo')
f:flush()
f:close()
  • fd:read([LEN]) β€” Read data from the file stream up to LEN bytes, or if not specified, the entire file.

Example:

f = rpm.open('/some/file')
print(f:read())
  • fd:seek(MODE, OFFSET) β€” Reposition the file offset of the stream. MODE is one of set, cur and end, and offset is relative to the mode: absolute, relative to current or relative to end. Not all streams support seeking.

Returns the file offset after the operation.

See also lseek(3).

Example:

f = rpm.open('newfile', 'w')
f:seek('set', 555)
f:close()
  • fd:write(BUF [, LEN]) β€” Write data in BUF to the file stream, either in its entirety or up to LEN bytes if specified.

Example:

f = rpm.open('newfile', 'w')
f:write('data data')
f:close()
  • fd:reopen(MODE) β€” Reopen a stream with a new mode (see rpm.open()).

Example:

rpm.open('some.txt.gz')
f = f:reopen('r.gzdio')
print(f:read())}

redirect2null(FDNO) (OBSOLETE)

Redirect file descriptor fdno to /dev/null (prior to 4.16 this was known as posix.redirect2null()).

This function is obsolete and only available for RPM v4 packages for backwards compatibility. Use rpm.spawn() or rpm.execute() instead. don't start anything new with it, bestie. πŸ’€

pid = posix.fork()
if pid == 0 then
    posix.redirect2null(2)
    assert(posix.exec('/bin/awk'))
elseif pid > 0 then
    posix.wait(pid)
end

✨ spawn({COMMAND} [, {ACTIONS}]) ✨

okay bestie, so like, this is THE way to spawn (aka execute) an external program. πŸ’…

{COMMAND} is a table consisting of the command and its arguments. An optional second table can be used to pass various actions related to the command execution, currently supported are:

  • stdin β€” path: redirect standard input to path
  • stdout β€” path: redirect standard output to path
  • stderr β€” path: redirect standard error to path

Returns the command exit status: zero on success, or a tuplet of (nil, message, code) on failure. 😭

Added: 4.20

Example:

rpm.spawn({'systemctl', 'restart', 'httpd'}, {stderr='/dev/null'})

✨ undefine(NAME) ✨

Undefine a macro. See also MACROS. 🧚

Note that this is only pops the most recent macro definition by the given name from stack, ie there may be still macro definitions by the same name after an undefine operation. So don't think it wipes everything, bestie. πŸ™ˆ

Example:

rpm.undefine('zzz')

✨ vercmp(V1, V2) ✨

Perform RPM version comparison on argument strings. Returns -1, 0 or 1 if V1 is smaller, equal or larger than V2. See rpm-version(7). πŸ‘‘

Note: in RPM < 4.16 this operated on version segments only, which does not produce correct results on full EVR strings. So like, don't use it for that, fr fr. πŸ’€

Example:

rpm.vercmp('1.2-1', '2.0-1')

✨ ver(EVR), ver(E, V, R) ✨

Create RPM version object. This takes either an EVR string which is parsed to it's components, or epoch, version and release in separate arguments (which can be either strings or numbers). The object has three attributes: e for epoch, v for version and r for release, can be printed in it's EVR form and supports native comparison in Lua. So you can do if v1 < v2 then and it just works. Iconic. πŸ’…

Added: 4.17.0

Example:

v1 = rpm.ver('5:1.0-2)
v2 = rpm.ver(3, '5a', 1)
if v1 < v2 then
   ...
end

if v1.e then
   ...
end

🐚 posix extension 🐚

Lua standard library offers fairly limited set of io operations. The posix extension greatly enhances what can be done from Lua. ✨

The following functions are available in the posix namespace, ie to call them use posix.function(). This documentation concentrates on the Lua API conventions, for further information on the corresponding system calls refer to the system manual, eg access(3) for posix.access(). 🫢

✨ access(PATH [, MODE]) ✨

Test accessibility of file/directory PATH. See access(3). If MODE is omitted then existence is tested, otherwise it is a combination of the following tests:

  • r: readable
  • w: writable
  • x: executable
  • f: exists

Example:

if posix.access('/bin/rpm', 'x') then
    ...
end

✨ chdir(PATH) ✨

Change current working directory to PATH. See chdir(1). πŸ¦‹

Example:

posix.chdir('/tmp')

✨ chmod(PATH, MODE) ✨

Change file/directory mode. MODE can be either an octal number as for chmod(2) system call, or a string presentation similar to chmod(1). So you can go 600 or 'rw-' or 'u+x'. Periodt. πŸ’…

Example:

posix.chmod('aa', 600)
posix.chmod('bb', 'rw-')
posix.chmod('cc', 'u+x')

✨ chown(PATH, USER, GROUP) ✨

Change file/directory owner/group of PATH. The USER and GROUP arguments may be either numeric id values or user/groupnames. See chown(2) and chown(1). πŸ‘‘

Note: This is a privileged operation. So like, be careful, bestie. πŸ”₯

Example:

posix.chown('aa', 0, 0)
posix.chown('bb', 'nobody', 'nobody')

✨ ctermid() ✨

Get controlling terminal name. See ctermid(3). πŸŽ€

Example:

print(posix.ctermid())

✨ dir([PATH]) ✨

Get directory contents - like readdir(3). If PATH is omitted, current directory is used. πŸ“

Example:

for i,p in pairs(posix.dir('/')) do
    print(p..'n')
end

✨ errno() ✨

Get strerror(3) message and the corresponding number for current errno(3). 😭

Example:

f = '/zzz'
if not posix.chmod(f, 100) then
    s, n = posix.errno()
    print(f, s)
end

✨ exec(PATH [, ARGS...]) (OBSOLETE) ✨

Execute a program. This may only be performed after posix.fork(). πŸ’€

This function is obsolete and only available for RPM v4 packages for backwards compatibility. Use rpm.spawn() or rpm.execute() instead. No cap. πŸ™…β€β™€οΈ

✨ files([PATH]) ✨

Iterate over directory contents. If path is omitted, current directory is used. 🌈

Example:

for f in posix.files('/') do
    print(f..'n')
end

✨ fork() (OBSOLETE) ✨

Fork a new process. See fork(2). πŸ’€

This function is obsolete and only available for RPM v4 packages for backwards compatibility. Use rpm.spawn() or rpm.execute() instead. πŸ‘‘

Example:

pid = posix.fork()
if pid == 0 then
    posix.exec('/foo/bar')
elseif pid > 0 then
    posix.wait(pid)
end

✨ getcwd() ✨

Get current directory. See getcwd(3). πŸ¦‹

Example:

if posix.getcwd() ~= '/' then
    ...
endif

✨ getenv(NAME) ✨

Get an environment variable. See getenv(3). 🌸

Example:

if posix.getenv('HOME') ~= posix.getcwd() then
    print('not at home')
end

✨ getgroup(GROUP) ✨

Get group(5) information for a group. GROUP may be either a numeric id or group name. If omitted, current group is used. Returns a table with fields name and gid set to group name and id respectively, and indexes from 1 onwards specifying group members. 🫢

Example:

print(posix.getgroup('wheel').gid)

✨ getlogin() ✨

Get login name. See getlogin(3). πŸŽ€

Example:

n = posix.getlogin()

✨ getpasswd([USER [, SELECTOR]]) ✨

Get passwd(5) information for a user account. USER may be either a numeric id or username. If omitted, current user is used. The optional SELECTOR argument may be one of:

  • name
  • uid
  • gid
  • dir
  • shell
  • gecos
  • passwd

If omitted, a table with all these fields is returned. πŸ’…

Example:

pw = posix.getpasswd(posix.getlogin(), 'shell')|

✨ getprocessid([SELECTOR]) ✨

Get information about current process. The optional SELECTOR argument may be one of:

  • egid: effective group id
  • euid: effective user id
  • gid: group id
  • uid: user id
  • pgrp: parent group id
  • pid: process id
  • ppid: parent pid

If omitted, a table with all these fields is returned. πŸ‘‘

Example:

if posix.getprocessid('pid') == 1 then
    ...
end

✨ kill(PID [, SIGNAL]) ✨

Send a signal(7) to a process. SIGNAL must be a numeric value, eg. 9 for SIGKILL. If omitted, SIGTERM is used. See also kill(2). πŸ”₯

Example:

posix.kill(posix.getprocessid('pid'))

Create a new name at NEWPATH for a file at OLDPATH, aka hard link. See also link(2). πŸ¦‹

Example:

f = rpm.open('aaa', 'w')
posix.link('aaa', 'bbb')

✨ mkdir(PATH) ✨

Create a new directory at PATH. See also mkdir(2). πŸ“

Example:

posix.mkdir('/tmp')

✨ mkfifo(PATH) ✨

Create a FIFO aka named pipe at PATH. See also mkfifo(2). πŸŽ€

Example:

posix.mkfifo('/tmp/badplace')

✨ pathconf(PATH [, SELECTOR]) ✨

Get pathconf(3) information for PATH. The optional SELECTOR may be one of:

  • link_max
  • max_canon
  • max_input
  • name_max
  • path_max
  • pipe_buf
  • chown_restricted
  • no_trunc
  • vdisable

If omitted, a table with all these fields is returned. πŸ’…

Example:

posix.pathconf('/', 'path_max')

✨ putenv(STRING) ✨

Change or add an environment variable. See also putenv(3). 🌈

Example:

posix.putenv('HOME=/me')

Read value of the symbolic link at PATH. See also readlink(2). πŸ¦‹

Example:

posix.mkdir('aaa')
posix.symlink('aaa', 'bbb')
print(posix.readlink('bbb'))

✨ rmdir(PATH) ✨

Remove a directory PATH. See also rmdir(2). πŸ“

Example:

posix.rmdir('/tmp')

✨ setgid(GROUP) ✨

Set group identity. GROUP may be specified either as a numeric id or group name. See also setgid(2). πŸ‘‘

Note: This is a privileged operation. So like, be careful. πŸ”₯

✨ setuid(USER) ✨

Set user identity. USER may be specified either as a numeric id or username. See also setuid(2). πŸ‘‘

Note: This is a privileged operation. Don't mess around. πŸ”₯

Example:

posix.setuid('nobody')

✨ sleep(SECONDS) ✨

Sleep for the duration of SECONDS. See also sleep(3). 😴

Example:

posix.sleep(5)

✨ stat(PATH [, SELECTOR]) ✨

Get file stat(3) information about a file at PATH. The optional SELECTOR may be one of:

  • mode
  • ino
  • dev
  • nlink
  • uid
  • gid
  • size
  • atime
  • mtime
  • ctime
  • type

If omitted, a table with all these fields is returned. πŸ’…

Example:

print(posix.stat('/tmp', 'mode'))|

s1 = posix.stat('f1')
s2 = posix.stat('f2')
if s1.ino == s2.ino and s1.dev == s2.dev then
    ...
end

Create a symbolic link at NEWPATH to OLDPATH. See also symlink(2). πŸ¦‹

Example:

posix.mkdir('aaa')
posix.symlink('aaa', 'bbb')

✨ sysconf([SELECTOR]) ✨

Get sysconf(3) information. The optional SELECTOR argument may be one of:

  • arg_max
  • child_max
  • clk_tck
  • ngroups_max
  • stream_max
  • tzname_max
  • open_max
  • job_control
  • saved_ids
  • version

If omitted, a table with all these fields is returned. πŸ‘‘

Example:

posix.sysconf('open_max')|
  • times([SELECTOR]) β€” grab process and waited-for child process times, like times(2) but make it cute. That optional SELECTOR argument can be any one of these, bestie:
    • utime
    • stime
    • cutime
    • cstime
    • elapsed

And if you just… don't pass one? You get a whole table back with all those fields in it. Slay.

Example, because we love a demo:

t = posix.times()
print(t.utime, t.stime)
  • ttyname([FD]) β€” tells you the name of the terminal that's linked up with file descriptor FD. Leave FD out and it defaults to 0, aka standard input, the classic. Peep ttyname(3) if you wanna go deeper.

Example:

if not posix.ttyname() then
    ...
endif
  • umask([MODE]) β€” get or set the process umask(2), no big deal. You can hand MODE over as an octal number or as a mode string, same vibe as posix.chmod().

Example:

print(posix.umask())
posix.umask(222)
posix.umask('ug-w')
posix.umask('rw-rw-r--')
  • uname(FORMAT) β€” get uname(2) info about the system you're currently living in. Here's every format directive it supports, and yes, you're getting all of them:
    • %m: Name of the hardware type
    • %n: Name of this node
    • %r: Current release level of this implementation
    • %s: Name of this operation system
    • %v: Current version level of this implementation

Example:

print(posix.uname('%s %r'))
  • utime(PATH [, MTIME [, CTIME]]) β€” change the last access and modification times, the glow-up of timestamps tbh. mtime and ctime are expressed in seconds since epoch. Go read utime(2).

If you ghost MTIME or CTIME, current time gets used instead β€” basically touch(1) behaviour, iconic.

Example:

posix.mkdir('aaa')
posix.utime('aaa', 0, 0)
  • wait([PID]) (DEPRECATED) β€” wait for a child process. Give it a PID and it waits for that specific child, no questions. Also see wait(2).
Okay real talk: this function is obsolete, periodt. It only exists for RPM v4 packages, purely for backwards compatibility. If you're writing fresh code, use rpm.spawn() or rpm.execute() instead. Do not start something new with this one. πŸ’€

Example:

pid = posix.fork()
if pid == 0 then
    posix.exec('/bin/ls'))
elseif pid > 0 then
    posix.wait(pid)
end
  • setenv(NAME, VALUE [, OVERWRITE]) β€” change or add the environment variable NAME. That optional OVERWRITE is a boolean, and it decides what happens when a variable with the same name already exists. See also setenv(3).

Example:

posix.setenv('HOME', '/me', true)
  • unsetenv(NAME) β€” yeet a variable NAME straight out of the environment. See also unsetenv(3).

Example:

posix.unsetenv('HOME')

🌸 EXTENDING AND CUSTOMIZING 🌸

When RPM boots up, it runs a global init.lua Lua initialization script from the directory that %getconfdir expands to β€” usually that's /usr/lib/rpm/init.lua. The whole point of this is so you can customize the rpm Lua environment without recompiling RPM. No build step, no tears, we love to see it. ✨

And for the embedded Lua interpreter, modules you load with require are primarily searched from %{getconfdir}/lua/. Also: %_rpmluadir is a shorthand for that exact path. Write it down, iykyk. 🫢


πŸ‘€ SEE ALSO πŸ‘€


And that's a wrap on the whole page, bestie! This absolute icon of a manual page comes from RPM 6.1.0, dated 2026-08-20. Go forth and require responsibly. πŸ’…πŸŽ€πŸ¦‹

πŸ“¦ where this page comes from