Skip to content

Extending Writing Lua Plugins

Leonard Ramminger edited this page May 6, 2026 · 7 revisions

Writing Lua Plugins

Prev: Choosing an Extension Model | Up: Extending ReqPack | Next: Building Registry Entries

Lua plugins are the main extension point for ReqPack. They implement package-manager behavior while ReqPack provides config, planning, audit integration, output handling, and remote execution. This page describes normal wrapper or proxy plugins loaded through LuaBridge, not hook scripts inside native .rqp packages.

Where Plugins Live

Local plugin layout:

plugins/
  <plugin-id>/
    <plugin-id>.lua
    bootstrap.lua        # optional

Important:

  • plugin implementation must expose global plugin table,
  • optional bootstrap hook is global bootstrap() function in bootstrap.lua, not plugin.bootstrap().

Examples in this repo:

  • plugins/dnf/dnf.lua
  • plugins/maven/maven.lua
  • plugins/java/java.lua
  • plugins/sys/sys.lua

Required Plugin Contract

ReqPack validates the Lua plugin contract at load time. These methods are required:

function plugin.getName() end
function plugin.getVersion() end
function plugin.getRequirements() end
function plugin.getCategories() end
function plugin.getMissingPackages(packages) end
function plugin.install(context, packages) end
function plugin.installLocal(context, path) end
function plugin.remove(context, packages) end
function plugin.update(context, packages) end
function plugin.list(context) end
function plugin.search(context, prompt) end
function plugin.info(context, packageName) end

Optional methods:

function plugin.init() end
function plugin.shutdown() end
function plugin.outdated(context) end
function plugin.resolvePackage(context, package) end
function plugin.resolveProxyRequest(context, request) end
function plugin.getSecurityMetadata() end

Optional static data:

plugin.fileExtensions = { ".rpm" }

plugin.fileExtensions is metadata table, not function.

Minimal Wrapper Example

plugin = {}

function plugin.getName()
  return "Example Manager"
end

function plugin.getVersion()
  return "1.0.0"
end

function plugin.getRequirements()
  return {}
end

function plugin.getCategories()
  return { "Example" }
end

function plugin.getMissingPackages(packages)
  return packages
end

function plugin.install(context, packages)
  context.tx.begin_step("install example packages")
  local result = context.exec.run("example-pm install ...")
  if not result.success then
    context.tx.failed("example install failed")
    return false
  end
  context.events.installed(packages)
  context.tx.success()
  return true
end

function plugin.installLocal(context, path)
  return false
end

function plugin.remove(context, packages)
  return true
end

function plugin.update(context, packages)
  return true
end

function plugin.list(context)
  return {}
end

function plugin.search(context, prompt)
  return {}
end

function plugin.info(context, packageName)
  return {}
end

The context API

ReqPack exposes a rich plugin context.

Basic plugin metadata

context.plugin.id
context.plugin.dir
context.plugin.script
context.plugin.bootstrap

Flags and host info

context.flags
context.host
context.proxy
context.repositories

Logging

context.log.debug("...")
context.log.info("...")
context.log.warn("...")
context.log.error("...")

Transaction and progress events

context.tx.status(42)
context.tx.progress(50)
context.tx.begin_step("resolve dependencies")
context.tx.commit()
context.tx.success()
context.tx.failed("something went wrong")

context.tx.progress(...) can also accept a richer payload with byte counters and rates.

Domain events

context.events.installed(payload)
context.events.deleted(payload)
context.events.updated(payload)
context.events.listed(payload)
context.events.searched(payload)
context.events.informed(payload)
context.events.outdated(payload)
context.events.unavailable(payload)

These events feed ReqPack's display and structured output layers.

Command execution and helper services

local result = context.exec.run("dnf repoquery curl")
local tmpDir = context.fs.get_tmp_dir()
local ok = context.net.download(url, destination)
context.artifacts.register({ type = "file", path = "/tmp/out" })

Important runtime boundaries:

  • context.fs in Lua plugins currently exposes temp-directory helper, not general copy/mkdir helpers.
  • context.net.download(...) returns success boolean.
  • context.artifacts.register(...) records artifact payload for ReqPack; shape is plugin-defined JSON-like table.
  • if you need richer filesystem helpers for install/remove hooks, that is native rqp hook runtime, not normal Lua plugin runtime.

There is also a global namespace:

local result = reqpack.exec.run("command -v dnf >/dev/null 2>&1")
local host = reqpack.host

Use context.exec.run(...) inside action methods when possible, because it carries the current item identity and display correlation.

Data Types You Receive

package in install, remove, update, resolvePackage

Relevant fields:

  • action
  • system
  • name
  • version
  • sourcePath
  • localTarget
  • flags
  • directRequest

request in resolveProxyRequest

  • action
  • system
  • packages
  • flags
  • outputFormat
  • outputPath
  • localPath
  • usesLocalTarget

PackageInfo for list, search, info, outdated

Useful fields include:

  • name, packageId, version, latestVersion
  • status, installed
  • summary, description
  • homepage, documentation, sourceUrl, repository
  • channel, section, packageType, architecture, license
  • dependencies, optionalDependencies, provides, conflicts, replaces, binaries, tags
  • extraFields for additional key/value output

You do not need to fill every field. Fill the ones your ecosystem can provide reliably.

getMissingPackages() Matters More Than It Looks

ReqPack uses getMissingPackages() during planning to avoid redundant work.

Good plugin behavior:

  • return only packages that still need action,
  • for installs: packages not already present,
  • for removes: packages that are actually present,
  • for updates: packages with available newer versions.

If this function is lazy and always returns everything, ReqPack still works, but planning quality and UX get worse.

resolvePackage() Improves Security and SBOM Quality

If your plugin can resolve a package into an exact version, implement resolvePackage().

This helps with:

  • vulnerability matching,
  • SBOM exports,
  • explicit package identity,
  • better audit decisions when users do not specify exact versions.

Without it, unresolved-version policy may block installs or reduce audit fidelity.

Testing a New Plugin Against Audit and SBOM

When you add a new ecosystem, this is the practical minimum validation path:

  1. implement resolvePackage() if exact version lookup is possible,
  2. return osvEcosystem from getSecurityMetadata() or configure security.osvEcosystemMap,
  3. run audit with a local feed path,
  4. run SBOM export for explicit packages and installed packages.

Useful commands:

ReqPack audit <system> <package> --osv-feed ./test-data/osv --osv-refresh always
ReqPack audit <system> --strict-ecosystem-mapping
ReqPack sbom <system> --format json

If audit fails with ecosystem-mapping or unresolved-version errors, fix those before calling the plugin production-ready.

resolveProxyRequest() for Proxy Plugins

Proxy plugins should not implement real package-manager behavior directly. They should transform the request and hand it to a real target system.

Minimal shape:

function plugin.resolveProxyRequest(context, request)
  return {
    targetSystem = "maven",
    flags = request.flags,
    packages = request.packages,
  }
end

Rules:

  • do not return both packages and localPath
  • do not resolve to yourself
  • stay within configured context.proxy.targets when present
  • if you return flags, they replace request flags for next resolved request

Security Metadata

If you want better trust and security behavior, implement getSecurityMetadata().

Example:

function plugin.getSecurityMetadata()
  return {
    role = "package-manager",
    capabilities = { "exec", "network" },
    ecosystemScopes = { "rpm" },
    writeScopes = {
      { kind = "temp" },
      { kind = "user-home-subpath", value = ".cache/dnf" },
    },
    networkScopes = {
      { host = "api.osv.dev", scheme = "https", pathPrefix = "/v1" },
    },
    privilegeLevel = "sudo",
    osvEcosystem = "RPM",
    purlType = "rpm",
    versionComparatorProfile = "rpm-evr",
    versionTokenPattern = "[A-Za-z0-9._+-]+",
    versionCaseInsensitive = false,
  }
end

This becomes especially important when security.requireThinLayer = true.

Best Practices for Lua Plugins

  • Keep the plugin thin. Let the real package manager do real package-manager work.
  • Emit events and transaction steps. They improve UX, remote output, and logs.
  • Implement installLocal() if your ecosystem supports local artifacts.
  • Implement resolvePackage() as soon as exact version resolution is possible.
  • Return realistic PackageInfo records. info and outdated should not be placeholders in mature plugins.
  • Prefer deterministic shell commands and parse outputs conservatively.
  • Use plugin.fileExtensions if local-file inference makes sense.
  • Test plugin through actual ReqPack commands because runtime contract is behavioral, not only structural.

Related Pages

Prev: Choosing an Extension Model | Up: Extending ReqPack | Next: Building Registry Entries

Clone this wiki locally