Files
yay/doc/lua.md
Jo 072503e2ec chore(docs): link examples to lua funcs (#2898)
* add doc/lua examples

* full url
2026-07-06 14:58:28 +02:00

18 KiB

Lua configuration (init.lua)

yay can optionally load a Lua configuration file, init.lua. init.lua overlays whatever is in config.json, and any command-line flag you pass still wins over init.lua.

yay uses the Lua 5.1 interpreter to run init.lua. The Lua standard library is available.

Location

init.lua is looked up, in order:

  1. $XDG_CONFIG_HOME/yay/init.lua
  2. $HOME/.config/yay/init.lua

Example init.lua

Editor support

Available from yay v13.0.2

yay ships a lua-language-server type definition file at meta/yay.d.lua. It declares the yay global, every yay.opt field, and all autocmd event payloads so your editor can provide completion, hover documentation, and catch typos such as unknown option names or misspelled event fields.

Setup

Point lua-language-server at the meta/ directory via workspace.library in your configuration file. With a .luarc.json in your project root, next to init.lua:

{
  "runtime": { "version": "Lua 5.1" },
  "workspace": {
    "library": ["/usr/share/yay/meta"]
  }
}

Replace /usr/share/yay/meta with the path to the meta/ directory in your local yay checkout or installation. The runtime version should be Lua 5.1 because yay runs init.lua through the gopher-lua Lua 5.1 interpreter.

lua-language-server is available for VS Code, Neovim, JetBrains, and other editors that support the Language Server Protocol.

Setting options with yay.opt

Available from yay v13.0.0

Assign to keys on the yay.opt table using the exact option names shown below.

yay.opt.bottom_up = false
yay.opt.clean_after = true
yay.opt.sort_by = "votes"
yay.opt.request_split_n = 150
yay.opt.editor = os.getenv("EDITOR") or "vi"

Unknown keys and values of the wrong type are treated as errors. yay aborts startup and reports the offending keys/values so misconfigurations fail fast.

Available options

Strings

aururl, aurrpcurl, build_dir, editor, editor_flags, makepkg_bin, makepkg_conf, pacman_bin, pacman_conf, redownload, rebuild, git_bin, gpg_bin, gpg_flags, mflags, sort_by, search_by, git_flags, remove_make, sudo_bin, sudo_flags

Strings (Available from yay v13.0.1)

answer_clean, answer_diff, answer_edit

Integers

request_split_n, completion_refresh_time, max_concurrent_downloads

Booleans

bottom_up, sudo_loop, devel, clean_after, keep_src, provides, pgp_fetch, clean_menu, diff_menu, edit_menu, combined_upgrade, use_ask, batch_install, single_line_results, separate_sources, debug, rpc, double_confirm

A ready-to-copy example lives at doc/init.lua.

Logging with yay.log

Available from yay v13.0.0

Lua config and hooks can write through yay's normal logger:

yay.log.debug("build dir:", yay.opt.build_dir)
yay.log.info("loaded init.lua")
yay.log.warn("skipping", "pkgname")
yay.log.error("policy check failed")

debug only prints when debug logging is enabled. error logs an error-level message and does not stop execution; use yay.abort("message") for controlled hook stops.

Requiring modules with require()

Available from yay v13.0.1

init.lua can pull in other Lua files with the standard require() function. yay prepends the directory that contains init.lua to package.path, so modules resolve relative to your yay config directory rather than the directory you run yay from.

Given this layout:

$XDG_CONFIG_HOME/yay/
  init.lua
  hooks/
    maintainer_change.lua

init.lua can do:

require("hooks.maintainer_change")

require("name") looks up, in order:

  1. <config_dir>/name.lua
  2. <config_dir>/name/init.lua

Dotted module names map onto the filesystem, so require("hooks.maintainer_change") loads <config_dir>/hooks/maintainer_change.lua. This lets you split hooks and helpers across multiple files and keep init.lua small.

Required modules run in the same Lua state as init.lua, so anything they register through yay.create_autocmd or assign to yay.opt takes effect just as if it were written inline.

Upgrade selection hooks

Available from yay v13.0.0

UpgradeSelect runs during yay -Syu after yay has built and sorted the upgrade graph, and before the native "Packages to exclude" menu is printed. The hook can return package names to exclude. By default, yay still shows the native menu after applying hook exclusions.

yay.create_autocmd("UpgradeSelect", {
  desc = "skip recently modified AUR upgrades",
  callback = function(event)
    local exclude = {}
    local recent_cutoff = os.time() - (3 * 24 * 60 * 60)
    for _, pkg in ipairs(event.data.upgrades) do
      if pkg.repository == "aur" and pkg.last_modified >= recent_cutoff then
        yay.log.debug("pre-excluding recently modified AUR package:", pkg.name)
        table.insert(exclude, pkg.name)
      end
    end

    return { exclude = exclude, skip_menu = false }
  end,
})

Multiple UpgradeSelect hooks run in registration order. Their exclude lists are unioned. If any hook returns skip_menu = true, yay applies all hook exclusions and skips the native menu. With skip_menu = false or no return value, hook exclusions are applied first and then the native menu is shown.

More examples: doc/examples/recently_modified.lua (pre-exclude recently modified AUR upgrades) and doc/examples/maintainer_change.lua (warn on AUR maintainer changes).

Returned exclusions must name packages from event.data.upgrades. Unknown names are treated as hook errors so typos do not silently upgrade the wrong package. Pulled dependencies are visible in event.data.pulled_dependencies, but they are removed only when pruning an excluded upgrade candidate requires it.

UpgradeSelect event

The callback receives this table:

{
  event = "UpgradeSelect",
  data = {
    upgrades = {
      {
        id = 3,
        name = "pkgname",
        base = "pkgbase",
        repository = "aur",
        local_version = "1.2.3-3",
        remote_version = "1.2.3-4",
        reason = "explicit",
        last_modified = 1700000000,
        maintainer = "username",
      },
    },
    pulled_dependencies = {
      {
        id = 0,
        name = "depname",
        base = "",
        repository = "core",
        local_version = "",
        remote_version = "1.0-1",
        reason = "dependency",
        last_modified = 0,
        maintainer = "",
      },
    },
  },
}

For selectable data.upgrades entries, id matches the number shown in the native menu. pulled_dependencies entries are shown separately by yay and use id = 0 because they are not directly selectable.

AUR pre-install hooks

Available from yay v13.0.0

init.lua can register hooks with a small autocmd API:

yay.create_autocmd("AURPreInstall", {
  desc = "inspect or modify AUR package files",
  callback = function(event)
    -- event.match is the package base.
    -- event.data has package metadata and local file paths.
  end,
})

AURPreInstall runs once per AUR package base, in sorted package-base order, after the AUR PKGBUILD repositories are downloaded and merged. It runs before the clean, diff, and edit menus, and before source downloads or builds.

Use yay.abort("message") for controlled policy stops without a Lua traceback. If a callback raises a Lua error, yay aborts the install before build work starts and includes the Lua traceback for debugging.

Changing fields in the Lua event table does not change yay's internal package state. Hooks can still edit files through Lua's normal io and os libraries; later menus and build steps will see those file changes.

AURPreInstall event

The callback receives this table:

{
  event = "AURPreInstall",
  match = "pkgbase",
  data = {
    base = "pkgbase",
    dir = "/path/to/build/pkgbase",
    pkgbuild_path = "/path/to/build/pkgbase/PKGBUILD",
    srcinfo_path = "/path/to/build/pkgbase/.SRCINFO",
    pkgbuild = "...PKGBUILD contents...",
    version = "1:1.2.3-4",
    last_modified = 1700000000,
    installed = true,
    packages = {
      {
        name = "pkgname",
        version = "1:1.2.3-4",
        local_version = "1:1.2.3-3",
        reason = "explicit",
        upgrade = true,
        devel = false,
      },
    },
    srcinfo = {
      pkgbase = "pkgbase",
      pkgver = "1.2.3",
      pkgrel = "4",
      epoch = "1",
      version = "1:1.2.3-4",
      pkgdesc = "description",
      url = "https://example.invalid",
      arch = { "x86_64" },
      license = { "MIT" },
      depends = { "glibc" },
      makedepends = { "go" },
      checkdepends = { "bats" },
      optdepends = { "pkg: optional feature" },
      provides = { "virtual-pkg" },
      conflicts = { "old-pkg" },
      replaces = { "older-pkg" },
    },
  },
}

data.packages contains the target packages for that base. Split packages are listed separately. reason is one of explicit, dependency, make_dependency, check_dependency, or unknown.

Example

yay.create_autocmd("AURPreInstall", {
  desc = "block forbidden sources and patch a PKGBUILD",
  callback = function(event)
    if event.data.pkgbuild:match("forbidden.example") then
      yay.log.warn(event.match .. ": forbidden source URL")
      yay.abort(event.match .. ": forbidden source URL")
    end

    if event.match == "demo-pkg" then
      local path = event.data.pkgbuild_path
      local f = assert(io.open(path, "r"))
      local body = f:read("*a")
      f:close()

      body = body:gsub("options=%('strip'%)", "options=('!strip')")

      f = assert(io.open(path, "w"))
      f:write(body)
      f:close()
    end
  end,
})

AUR post-download hooks

Available from yay v13.0.0

AURPostDownload runs once per AUR package base, in sorted package-base order, after yay runs makepkg --verifysource for package sources and before compatibility checks, PGP key import prompts, builds, or package installs.

Use yay.abort("message") to stop the operation without a Lua traceback. AURPostDownload receives the same payload shape as AURPreInstall; only the event value differs.

AURPostDownload event

The callback receives this table:

{
  event = "AURPostDownload",
  match = "pkgbase",
  data = {
    base = "pkgbase",
    dir = "/path/to/build/pkgbase",
    pkgbuild_path = "/path/to/build/pkgbase/PKGBUILD",
    srcinfo_path = "/path/to/build/pkgbase/.SRCINFO",
    pkgbuild = "...PKGBUILD contents...",
    version = "1:1.2.3-4",
    last_modified = 1700000000,
    installed = true,
    packages = { ... },
    srcinfo = { ... },
  },
}

Example

yay.create_autocmd("AURPostDownload", {
  desc = "block forbidden source URLs after download",
  callback = function(event)
    if event.data.pkgbuild:match("forbidden.example") then
      yay.abort(event.match .. ": forbidden source URL")
    end
  end,
})

Post-install hooks

Available from yay v13.0.0

PostInstall fires once after a successful install/upgrade transaction, before yay exits. It is skipped when --downloadonly (-w) is used. Because the installation is already complete when the callback runs, calling yay.abort logs the message but cannot roll back anything.

PostInstall event

{
  event = "PostInstall",
  data = {
    packages = {
      {
        name          = "pkgname",
        version       = "1.2.3-1",    -- resolved version
        local_version = "1.0.0-1",    -- previously installed ("" if not installed)
        source        = "aur",        -- "aur" | "sync" | "local" | "srcinfo" | "missing"
        reason        = "explicit",   -- "explicit" | "dependency" | "make_dependency" | "check_dependency" | "unknown"
      },
      -- one entry per package yay resolved; sorted alphabetically
    },
  },
}

The packages array covers every node yay resolved into the transaction (all sources, all topo layers). Transitive repo dependencies pulled in by pacman but not explicitly tracked by yay are not included. The callback is fire-and-forget: no return value is read.

More examples: doc/examples/install_log.lua (append every installed/upgraded package to a persistent log file).

Example

yay.create_autocmd("PostInstall", {
  desc = "log every package yay installed",
  callback = function(event)
    for _, pkg in ipairs(event.data.packages) do
      yay.log.info(pkg.name .. " " .. pkg.version .. " (" .. pkg.source .. ")")
    end
  end,
})

Search-filter hooks

SearchFilter runs during yay -Ss and the yay -S number menu, after results are ranked and sorted but before they are displayed. The callback receives the full ordered result list and may return a filtered or reordered subset. Returning nil (or nothing) leaves the list unchanged.

Multiple SearchFilter hooks chain: each hook receives the output of the previous hook. An unknown (source, name) pair in the return table is a hard error; duplicate refs are deduplicated first-wins. Hook errors are logged and the unfiltered results are shown rather than aborting the command.

SearchFilter event

Available from yay v13.0.0

{
  event = "SearchFilter",
  data = {
    results = {
      {
        source          = "aur",      -- "aur" or the pacman DB name (e.g. "core", "extra")
        name            = "pkgname",
        description     = "A useful package",
        base            = "pkgbase",
        votes           = 123,        -- -1 for sync packages
        popularity      = 1.23,       -- -1 for sync packages
        first_submitted = 1700000000, -- -1 for sync packages
        last_modified   = 1700000001, -- -1 for sync packages
      },
      -- …
    },
  },
}

The callback must return nil or an array of {source=, name=} tables. Every (source, name) pair must exist in the input; unknown pairs are an error.

More examples: doc/examples/hide_first_submitted.lua (hide AUR packages submitted in the last 14 days).

Example

yay.create_autocmd("SearchFilter", {
  desc = "show only AUR results",
  callback = function(event)
    local out = {}
    for _, r in ipairs(event.data.results) do
      if r.source == "aur" then
        out[#out + 1] = { source = r.source, name = r.name }
      end
    end
    return out
  end,
})

Render hooks

RenderAUR and RenderSync fire once per result row during yay -Ss and the yay <pkg> number menu, after results are ranked, sorted, and (optionally) filtered. The callback receives per-package metadata and may return a string that fully replaces what yay would print for that row. Returning nil (or nothing) falls back to yay's built-in formatting.

The menu number prefix (magenta index shown in the number menu) is always prepended by yay itself; the callback controls only the package portion of the line. -q/--quiet (minimal verbosity) is unaffected — name-only output never calls these hooks. Hook errors are logged and that row falls back to default rendering rather than aborting the command.

Multiple hooks of the same event run in registration order; the last hook that returns a non-nil string wins. A later hook returning nil does not undo an earlier hook's string.

More examples: doc/examples/single_line.lua (reproduces yay.opt.single_line_results with colored, single-line AUR and sync results).

RenderAUR event

Available from yay v13.1.0

{
  event = "RenderAUR",
  data = {
    name            = "pkgname",
    version         = "1.0-1",
    description     = "A useful package",
    base            = "pkgbase",
    votes           = 42,
    popularity      = 1.23,
    maintainer      = "alice",
    out_of_date     = 0,        -- 0 if current; Unix timestamp if flagged
    first_submitted = 1700000000,
    last_modified   = 1700000001,
    local_version   = "",       -- installed version, or "" when not installed
  },
}

RenderSync event

Available from yay v13.1.0

{
  event = "RenderSync",
  data = {
    repository    = "extra",   -- pacman DB name (e.g. "core", "extra")
    name          = "pkgname",
    description   = "A useful package",
    version       = "2.0-1",
    groups        = {},        -- array of group strings, often empty
    local_version = "",        -- installed version, or "" when not installed
  },
}

Example

yay.create_autocmd("RenderAUR", {
  desc = "compact AUR search lines",
  callback = function(event)
    local d = event.data
    local tag = d.local_version ~= "" and (" [installed: " .. d.local_version .. "]") or ""
    return string.format("aur/%s %s (+%d)%s\n    %s",
      d.name, d.version, d.votes, tag, d.description)
  end,
})

yay.create_autocmd("RenderSync", {
  desc = "compact sync search lines",
  callback = function(event)
    local d = event.data
    local tag = d.local_version ~= "" and (" [installed: " .. d.local_version .. "]") or ""
    return string.format("%s/%s %s%s\n    %s",
      d.repository, d.name, d.version, tag, d.description)
  end,
})