Repository navigation
Extending Lua Plugin Cookbook
Prev: Plugin Manifest Dependencies | Up: Extending ReqPack | Next: Testing Lua Plugins
This page shows practical recipes for common plugin patterns. For exact runtime contract, read Writing Lua Plugins.
Good baseline shape:
plugin = {}
function plugin.getName()
return REQPACK_PLUGIN_ID
end
function plugin.getVersion()
return "1.0.0"
end
function plugin.getRequirements()
return {}
end
function plugin.getCategories()
return { "test", "system" }
end
function plugin.getMissingPackages(packages)
return packages or {}
end
function plugin.install(context, packages)
context.tx.begin_step("install packages")
local result = context.exec.run("demo-pm install " .. packages[1].name)
if not result.success then
context.tx.failed("install failed")
return false
end
context.events.installed({ name = packages[1].name })
context.tx.success()
return true
end
function plugin.installLocal(context, path)
return context.exec.run("demo-pm install-local " .. path).success
end
function plugin.remove(context, packages)
return context.exec.run("demo-pm remove " .. packages[1].name).success
end
function plugin.update(context, packages)
return context.exec.run("demo-pm update " .. packages[1].name).success
end
function plugin.list(context)
return {}
end
function plugin.search(context, prompt)
return {}
end
function plugin.info(context, packageName)
return { name = packageName, version = "1.0.0" }
end
function plugin.init()
return true
end
function plugin.shutdown()
return true
end
return pluginPattern:
- metadata methods stay side-effect free
- action methods do real work
- use
context.tx.*andcontext.events.*around mutations - prefer
context.exec.run(...)overreqpack.exec.run(...)in action methods
ReqPack can batch many packages into one action call. Do not assume exactly one package unless plugin intentionally only supports that.
Simple batch pattern:
function plugin.install(context, packages)
if packages == nil or #packages == 0 then
return true
end
local names = {}
for _, pkg in ipairs(packages) do
table.insert(names, pkg.name)
end
context.tx.begin_step("install packages")
local result = context.exec.run("demo-pm install " .. table.concat(names, " "))
if not result.success then
context.tx.failed("install failed")
return false
end
context.events.installed(packages)
context.tx.success()
return true
endWhen plugin must compute one command per package, build command list first, then execute joined command or loop explicitly.
ReqPack may hand versioned specs as name@version in package name space depending on source.
Keep parsing local and boring.
local function split_name_version(pkg)
local raw = tostring(pkg.name or "")
local name, embedded = raw:match("^(.+)@([^@]+)$")
if name == nil then
return raw, pkg.version
end
if pkg.version ~= nil and pkg.version ~= "" then
return name, pkg.version
end
return name, embedded
endUse explicit pkg.version first when already populated.
For list, search, info, and outdated, return structured records and emit matching events.
Example:
function plugin.info(context, packageName)
local result = context.exec.run("demo-pm info " .. packageName)
if not result.success then
return {}
end
local item = {
name = packageName,
version = "4.5.6",
description = result.stdout,
}
context.events.informed(item)
return item
endWhy:
- better user output
- stronger
test-pluginassertions - better audit/SBOM follow-on behavior when plugin later adds exact resolution
Use context.host, not reqpack.host, when action should reflect current request-time snapshot.
Example:
function plugin.getRequirements()
return {}
end
function plugin.install(context, packages)
local osFamily = context.host.platform.osFamily
local arch = context.host.platform.arch
if osFamily == "linux" and arch == "x86_64" then
return context.exec.run("demo-pm install-linux64 " .. packages[1].name).success
end
return context.exec.run("demo-pm install-generic " .. packages[1].name).success
endUseful fields include:
context.host.os.idcontext.host.os.distroIdcontext.host.cpu.logicalCorescontext.host.cpu.physicalCorescontext.host.memory.totalBytes
If host data looks stale while testing, run rqp host refresh.
Use this when one plugin bundle contains multiple internal drivers or profiles, each with its own dependency manifest.
Root plugin manifest:
return {
apiVersion = 1,
depends = { "dnf:lua" },
manifests = {
"drivers/ffmpeg/reqpack.lua",
"drivers/vllm/reqpack.lua",
},
}Nested driver manifest at drivers/ffmpeg/reqpack.lua:
return {
packages = {
{ system = "dnf", name = "ffmpeg" },
},
}Runtime lookup from run.lua:
function plugin.install(context, packages)
local manifest = reqpack.manifest.load("drivers/ffmpeg")
if manifest == nil then
context.tx.failed("ffmpeg manifest missing")
return false
end
local entry = manifest.packages[1]
context.log.info("installing driver dependency " .. entry.system .. ":" .. entry.name)
return true
endRuntime provisioning after you know the manifest path (ReqPack v1.3.1+):
function plugin.install(context, packages)
local driver_dir = "/home/user/.local/share/my-plugin/drivers/ffmpeg"
if not context.manifest.ensure(driver_dir) then
context.tx.failed("failed to ensure ffmpeg driver dependencies")
return false
end
return build_driver(driver_dir)
endRules:
- declarative
depends,packages, andmanifestsare expanded by the planner as ENSURE dependencies beforeplugin.install() -
reqpack.manifest.load(path)is for runtime inspection or branching inside plugin logic -
context.manifest.ensure(path)is for installing deps from a manifest at runtime, including paths outside the plugin bundle - nested paths are relative to
context.plugin.dir/REQPACK_PLUGIN_DIR
See Plugin Manifest Dependencies.
Use this when the source of truth for driver dependencies lives outside the installed plugin bundle — for example in a separate driver repository that your plugin downloads during plugin.install().
Driver manifest in the external repo at ffmpeg/reqpack.lua:
return {
packages = {
{ system = "dnf", name = "ffmpeg-devel" },
{ system = "dnf", name = "gcc" },
},
}Plugin install flow:
function plugin.install(context, packages)
local driver_id = packages[1].name
local driver_dir = download_and_extract_driver(driver_id)
if driver_dir == nil then
context.tx.failed("driver download failed")
return false
end
if not context.manifest.ensure(driver_dir) then
context.tx.failed("driver dependency ensure failed")
return false
end
return build_driver(driver_dir)
endRules:
- do not rely on declarative root
manifests = { "drivers/ffmpeg/reqpack.lua" }if that file is not shipped inside the installed plugin bundle - use an absolute path or a directory path after download;
context.manifest.ensure(driver_dir)resolvesdriver_dir/reqpack.lua - keep declarative root deps only for tools the plugin always needs before any driver-specific work
When your plugin downloads a catalog .rqp archive, prefer the native runtime handoff:
function plugin.install(context, packages)
local archive_path = download_catalog_rqp(packages[1])
if archive_path == nil then
return false
end
return context.rqp.installLocal(archive_path)
endDo not shell out to context.exec.run("rqp install " .. archive_path) when ReqPack v1.3.1+ is available.
Proxy plugins should not try to act like real package managers after resolution fails. Resolve once, return target system, packages or local path, and flags.
Example pattern:
local function normalized_target(value)
local target = tostring(value or ""):lower()
if target == "" then
return nil
end
return target
end
function plugin.resolveProxyRequest(context, request)
local proxy = context.proxy or {}
local target = normalized_target(proxy.default)
if target == nil and proxy.targets ~= nil then
target = normalized_target(proxy.targets[1])
end
if target == nil then
context.log.error("proxy could not choose target")
return nil
end
local resolution = {
targetSystem = target,
flags = request.flags,
}
if request.usesLocalTarget then
resolution.localPath = request.localPath
else
resolution.packages = request.packages
end
return resolution
endRemember source-enforced rules:
- max proxy depth is
4 - cycles are rejected
- self-target is rejected
- returning
nilis failure, not pass-through - returned
packagesandlocalPathare mutually exclusive
Add this as soon as plugin can resolve exact package identity.
Minimal pattern:
function plugin.resolvePackage(context, package)
local result = context.exec.run("demo-pm exact-version " .. package.name)
if not result.success then
return nil
end
return {
action = package.action,
system = package.system,
name = package.name,
version = result.stdout:gsub("%s+$", ""),
sourcePath = package.sourcePath,
localTarget = package.localTarget,
flags = package.flags,
}
endThis improves:
- audit matching accuracy
- SBOM exactness
- unresolved-version handling
Use exec rules when plugin must react to command output while command is still running.
Example:
local rules = {
initial = "default",
rules = {
{
state = "default",
source = "line",
regex = "^Loaded (.+)$",
repeat = true,
actions = {
{ type = "log", level = "info", message = "${1}" },
},
},
{
source = "line",
regex = "^Progress ([0-9]+)%%$",
actions = {
{ type = "progress", percent = "${1}" },
},
},
},
}
local result = context.exec.run("demo-pm install huge-package", rules)Use line rules first. Move to PTY/screen rules only when tool renders interactive full-screen or prompt-driven output.
Important:
- malformed rule schema fails command immediately
- any
screenrule orsendaction switches runner to PTY mode - placeholders like
${1}use regex captures
Only add plugin.pack() when ecosystem has genuine native package build flow.
If you are building ReqPack-native .rqp, use builtin rqp pack instead.
Minimal pattern:
function plugin.pack(context, projectPath, outputPath, flags)
local result = context.exec.run("demo-pack build --project " .. projectPath .. " --output " .. outputPath)
if not result.success then
return false
end
context.artifacts.register(outputPath)
return true
endRules worth remembering:
- during
plugin.pack(), ReqPack temporarily allows writes underprojectPath - register artifact path strings with
context.artifacts.register(...) - if plugin reports success for explicit output path but artifact file is missing, ReqPack treats that as failure
Happy-path list case:
return {
name = "system fixture list",
request = {
action = "list",
system = "demo-system",
},
fakeExec = {
{
match = "demo-pm list",
exitCode = 0,
stdout = "alpha entry",
stderr = "",
success = true,
}
},
expect = {
success = true,
commands = { "demo-pm list" },
stdout = { "alpha entry" },
events = { "listed" },
resultCount = 1,
resultName = "alpha",
resultVersion = "1.2.3",
}
}Happy-path info case:
return {
name = "system fixture info",
request = {
action = "info",
system = "demo-system",
prompt = "delta",
},
fakeExec = {
{
match = "demo-pm info delta",
exitCode = 0,
stdout = "delta details",
stderr = "",
success = true,
}
},
expect = {
success = true,
commands = { "demo-pm info delta" },
stdout = { "delta details" },
events = { "informed" },
resultCount = 1,
resultName = "delta",
resultVersion = "4.5.6",
}
}Recommended mix for each plugin:
- one happy-path query case
- one mutating success case
- one mutating failure case
- one payload-format-sensitive case if parser logic matters
- keep wrapper thin; underlying package manager should still own package-manager semantics
- keep
getName(),getVersion(),getSecurityMetadata(), andplugin.fileExtensionssafe beforeinit() - implement
getMissingPackages()carefully because planning and recovery depend on it - prefer exact result records over placeholder query output in mature plugins
- use real smoke tests after hermetic
test-plugincoverage
- Writing Lua Plugins
- Plugin Manifest Dependencies
- Testing Lua Plugins
- Building Registry Entries
- Security, Audit, and SBOM
Prev: Plugin Manifest Dependencies | Up: Extending ReqPack | Next: Testing Lua Plugins
- User Guide
- Getting Started
- Command Reference
- Configuration
- Configuration Reference
- Security, Audit, and SBOM
- Output and Report Formats
- Remote Mode
- Remote Protocol Reference
- Using Native
rqpPackages - Troubleshooting