Skip to content

Extending Plugin Manifest Dependencies

Leonard Ramminger edited this page Sep 26, 2026 · 2 revisions

Plugin Manifest Dependencies

Prev: Writing Lua Plugins | Up: Extending ReqPack | Next: Lua Plugin Cookbook

ReqPack now supports plugin-owned reqpack.lua manifests in three ways:

  1. Declarative bundle dependencies in the plugin root reqpack.lua
  2. Runtime lookup from plugin Lua through reqpack.manifest
  3. Runtime provisioning from plugin Lua through context.packages, context.manifest.ensure, and context.rqp (ReqPack v1.3.1+)

This page explains how plugin authors declare dependencies and how ReqPack resolves nested manifest files inside a plugin bundle.

This is separate from:

  • Project manifests used by rqp install . and rqp audit .
  • Native .rqp package hook manifests used inside .rqp archives

Those formats live in the user guide. This page is for plugin authors.

Three reqpack.lua Roles

ReqPack uses the filename reqpack.lua in several places, but not always with the same schema.

Location Schema Used for
Project directory packages = { ... } rqp install ., rqp audit ., snapshots
Plugin bundle root apiVersion = 1, optional depends, packages, manifests Plugin dependency provisioning
Nested path inside plugin bundle packages = { ... } Driver-specific or feature-specific plugin requirements
Native .rqp package hooks = { ... } Install/remove/update hooks for .rqp packages

Do not mix these schemas in one file unless you know exactly which fields belong together.

Declarative Plugin Dependencies

Every valid plugin bundle still needs a root reqpack.lua with:

return {
  apiVersion = 1,
}

On top of that, ReqPack supports three dependency surfaces in the same plugin-root manifest.

1. depends

Compact dependency specs for the planner.

return {
  apiVersion = 1,
  depends = {
    "curl",
    "dnf:python3@3.12",
    "npm:typescript",
  },
}

Accepted forms:

  • package
  • package@version
  • system:package
  • system:package@version

ReqPack expands these into ENSURE dependency packages before your plugin runs.

2. packages

Project-style manifest entries declared directly in the plugin root manifest.

return {
  apiVersion = 1,
  packages = {
    { system = "dnf", name = "gcc" },
    { system = "npm", name = "typescript", version = "5.6.0" },
  },
}

Each entry must contain at least:

  • system
  • name

Optional fields:

  • version
  • flags

ReqPack treats these the same way as entries loaded from nested project manifests.

3. manifests

Relative paths to additional project-style manifest files inside the plugin bundle.

return {
  apiVersion = 1,
  manifests = {
    "drivers/ffmpeg/reqpack.lua",
    "drivers/vllm/reqpack.lua",
  },
}

Each referenced file should use the normal project manifest format:

return {
  packages = {
    { system = "dnf", name = "ffmpeg" },
  },
}

ReqPack resolves these paths relative to the plugin bundle directory, not relative to the current working directory.

Example layout:

plugins/
  wuji/
    metadata.json
    reqpack.lua
    run.lua
    scripts/
      install.lua
      remove.lua
    drivers/
      ffmpeg/
        reqpack.lua
      vllm/
        reqpack.lua

Example root manifest:

return {
  apiVersion = 1,
  depends = {
    "dnf:lua",
  },
  manifests = {
    "drivers/ffmpeg/reqpack.lua",
    "drivers/vllm/reqpack.lua",
  },
  packages = {
    { system = "dnf", name = "curl" },
  },
}

When a user runs:

rqp install wuji demo-package

ReqPack plans:

  • the requested wuji package
  • dnf:lua from depends
  • dnf:curl from root packages
  • all packages from nested driver manifests

All of these are provisioned as plugin dependency ENSURE steps before the main install work runs.

Runtime Lookup From Plugin Lua

Inside run.lua, plugins can resolve and load manifest files from their own bundle through the global reqpack.manifest namespace.

Available functions:

Function Returns Purpose
reqpack.manifest.resolve(path) absolute path string or nil Resolve a manifest path relative to REQPACK_PLUGIN_DIR
reqpack.manifest.load(path) table or nil Resolve and parse a project-style manifest

Path rules:

  • explicit paths starting with . or / are resolved normally
  • bare paths such as drivers/ffmpeg/reqpack.lua are resolved relative to the plugin directory
  • directory arguments such as drivers/ffmpeg resolve to drivers/ffmpeg/reqpack.lua

Example inside plugin.install():

function plugin.install(context, packages)
  local manifest = reqpack.manifest.load("drivers/ffmpeg")
  if manifest == nil then
    context.tx.failed("ffmpeg driver manifest missing")
    return false
  end

  for _, entry in ipairs(manifest.packages) do
    context.log.info("driver requires " .. entry.system .. ":" .. entry.name)
  end

  -- continue with install logic
  return true
end

reqpack.manifest.load(path) returns a table like:

{
  path = "/home/user/.local/share/reqpack/plugins/wuji/drivers/ffmpeg/reqpack.lua",
  packages = {
    {
      system = "dnf",
      name = "ffmpeg",
    },
  },
}

If the file is missing, invalid, or has no packages table, the function returns nil.

Use this API when plugin logic needs manifest contents at runtime, for example:

  • choosing a driver-specific dependency set,
  • validating that a bundled manifest exists before install,
  • reading feature flags from nested manifest entries.

If you only want ReqPack to provision dependencies automatically before your plugin runs, prefer declarative depends, packages, or manifests in the plugin root manifest instead of loading them manually in Lua.

Runtime Provisioning From Plugin Lua

Since ReqPack v1.3.1, plugins can ask the core executor to install dependencies during plugin.install() or plugin.installLocal() — without shelling out to rqp install ....

These APIs are available on context inside action methods. They run in-process through the same executor that invoked your plugin.

API Returns Purpose
context.packages.ensure(packages) bool Install missing packages from a project-style package table
context.manifest.ensure(path) bool Resolve a manifest path, load its packages, then ensure them
context.rqp.installLocal(path) bool Hand a local .rqp file or directory to the native rqp plugin

Package table shape matches project manifests:

{
  { system = "dnf", name = "ffmpeg-devel" },
  { system = "dnf", name = "gcc", version = "14.0" },
}

Path rules for context.manifest.ensure(path):

  • explicit paths starting with . or / resolve from the filesystem (for downloaded driver trees)
  • bare paths such as drivers/ffmpeg resolve relative to context.plugin.dir
  • missing manifest or empty packages table is treated as success (nothing to ensure)

Example: driver downloaded outside the plugin bundle (Wuji-style flow):

function plugin.install(context, packages)
  local driver_dir = download_driver_to_local_store(packages[1].name)
  if driver_dir == nil then
    return false
  end

  -- read reqpack.lua from the downloaded driver tree, then ensure OS deps
  if not context.manifest.ensure(driver_dir) then
    context.tx.failed("driver dependency ensure failed")
    return false
  end

  return build_driver(driver_dir)
end

Example: install a downloaded .rqp archive in-process:

function plugin.install(context, packages)
  local archive_path = download_catalog_rqp(packages[1])
  if archive_path == nil then
    return false
  end

  if not context.rqp.installLocal(archive_path) then
    context.tx.failed("rqp installLocal failed")
    return false
  end

  return true
end

When to use which mechanism

Goal Mechanism
Always provision the same deps before any install declarative depends / packages / manifests in plugin root
Provision deps for one selected driver/profile at runtime context.manifest.ensure(path) after you know the path
Read manifest contents for branching/logging only reqpack.manifest.load(path)
Install a local .rqp from plugin logic context.rqp.installLocal(path)
Legacy fallback / bootstrap context.exec.run("rqp install ...")

Important differences from declarative manifests:

  • declarative entries must live inside the installed plugin bundle and are expanded for all listed manifests at plan time
  • context.manifest.ensure(path) can target any path on disk, including trees your plugin just downloaded
  • declarative provisioning happens before plugin.install(); runtime ensure happens inside your install logic

Do not shell out to rqp install ... for normal dependency or .rqp work when these context APIs are available.

How Resolution Works

ReqPack resolves plugin manifest paths in the core layer.

Resolution order for reqpack.manifest.resolve(path) and planner dependency expansion:

  1. If the argument is an explicit path (. or /), resolve it from the current filesystem context.
  2. Otherwise resolve it relative to the plugin bundle directory.
  3. If the resolved path is a directory, append /reqpack.lua.
  4. If the resolved path is already a reqpack.lua file, use it directly.

Core entry points:

  • resolve_manifest_path(...) for general manifest path resolution
  • resolve_plugin_manifest_path(...) for plugin-relative resolution
  • ManifestLoader::load(...) for strict project-manifest parsing
  • plugin_bundle_project_packages(...) for planner dependency expansion

The CLI reuses the same core resolver for project manifest arguments such as:

rqp install .
rqp audit ./reqpack.lua

That CLI path is still project-oriented. Plugin-relative resolution is primarily for plugin bundles and reqpack.manifest.

Recommended Patterns

Thin wrapper with one tool dependency

Use depends:

return {
  apiVersion = 1,
  depends = { "curl" },
}

Plugin with multiple internal drivers

Use nested manifests:

return {
  apiVersion = 1,
  manifests = {
    "drivers/ffmpeg/reqpack.lua",
    "drivers/vllm/reqpack.lua",
  },
}

Plugin that needs both compact specs and structured entries

Combine all three surfaces:

return {
  apiVersion = 1,
  depends = { "dnf:lua" },
  packages = {
    { system = "dnf", name = "make" },
  },
  manifests = {
    "profiles/server/reqpack.lua",
  },
}

Runtime-only inspection

Use reqpack.manifest.load(...) inside action methods when install logic must branch on manifest contents.

Do not duplicate the same dependency list in both declarative manifest fields and manual Lua provisioning unless you have a deliberate reason.

Keep plugin.getRequirements() Aligned

plugin.getRequirements() is still part of the runtime plugin contract.

Best practice:

  • treat plugin root reqpack.lua as source of truth for shipped dependencies
  • keep plugin.getRequirements() consistent with that manifest
  • use nested manifests when different plugin features need different dependency sets

If bundle manifest and getRequirements() diverge, planning and recovery behavior becomes harder to reason about.

Testing

Hermetic plugin tests can assert manifest lookup directly:

function plugin.install(context, packages)
  local resolved = reqpack.manifest.resolve("drivers/tool/reqpack.lua")
  local manifest = reqpack.manifest.load("drivers/tool")
  return resolved ~= nil
    and manifest ~= nil
    and manifest.packages[1].name == "curl"
end

For dependency provisioning behavior, run a real smoke test:

rqp install <your-plugin> <package>

Then verify that ENSURE steps for declared dependencies appear before the main plugin action.

For runtime context.manifest.ensure(...) or context.rqp.installLocal(...), verify the missing OS packages or .rqp install happens inside the plugin action itself rather than as separate top-level ENSURE steps planned from the plugin root manifest.

Related Pages

Prev: Writing Lua Plugins | Up: Extending ReqPack | Next: Lua Plugin Cookbook

Clone this wiki locally