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 scriptletargindexes are off by one from the general expectation based on traditional scriptlet arguments. The argument containing the number of installed package instances isarg[2], and the similar argument for trigger targets isarg[3]β versus the traditional$1and$2in 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 appendw: open for writing, truncater: open for reading (default)+: open for reading and writingx: 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 toLENbytes, 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.MODEis one ofset,curandend, 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 inBUFto the file stream, either in its entirety or up toLENbytes if specified.
Example:
f = rpm.open('newfile', 'w')
f:write('data data')
f:close()
fd:reopen(MODE)β Reopen a stream with a new mode (seerpm.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. Userpm.spawn()orrpm.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 pathstdoutβ path: redirect standard output to pathstderrβ 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: readablew: writablex: executablef: 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:
nameuidgiddirshellgecospasswd
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 ideuid: effective user idgid: group iduid: user idpgrp: parent group idpid: process idppid: 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'))
β¨ link(OLDPATH, NEWPATH) β¨
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_maxmax_canonmax_inputname_maxpath_maxpipe_bufchown_restrictedno_truncvdisable
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')
β¨ readlink(PATH) β¨
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:
modeinodevnlinkuidgidsizeatimemtimectimetype
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
β¨ symlink(OLDPATH, NEWPATH) β¨
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_maxchild_maxclk_tckngroups_maxstream_maxtzname_maxopen_maxjob_controlsaved_idsversion
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, liketimes(2)but make it cute. That optional SELECTOR argument can be any one of these, bestie:utimestimecutimecstimeelapsed
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 to0, aka standard input, the classic. Peepttyname(3)if you wanna go deeper.
Example:
if not posix.ttyname() then
...
endif
umask([MODE])β get or set the processumask(2), no big deal. You can hand MODE over as an octal number or as a mode string, same vibe asposix.chmod().
Example:
print(posix.umask())
posix.umask(222)
posix.umask('ug-w')
posix.umask('rw-rw-r--')
uname(FORMAT)β getuname(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 readutime(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 seewait(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, userpm.spawn()orrpm.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 alsosetenv(3).
Example:
posix.setenv('HOME', '/me', true)
unsetenv(NAME)β yeet a variable NAME straight out of the environment. See alsounsetenv(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
- ubuntu stonking: rpm 6.1.0-1 Β· GPL-2.0-or-later
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms