feat: Add AURPostDownload hook (#2856)

* run hooks after source download

* lint

* nitpick: remove event table indirection
This commit is contained in:
Jo
2026-06-15 18:10:41 +02:00
committed by GitHub
parent 421b65bf43
commit 16479dcaf3
9 changed files with 353 additions and 25 deletions

View File

@@ -73,7 +73,7 @@ yay.opt.double_confirm = true -- Ask for confirmation before and after builds du
-- end
-- end
--
-- return { exclude = exclude, skip_menu = true }
-- return { exclude = exclude, skip_menu = false }
-- end,
-- })
--
@@ -95,3 +95,15 @@ yay.opt.double_confirm = true -- Ask for confirmation before and after builds du
-- -- f:close()
-- end,
-- })
--
-- Run Lua after yay downloads/verifies package sources and before builds or
-- installs. AURPostDownload receives the same payload shape as AURPreInstall.
--
-- 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,
-- })

View File

@@ -83,7 +83,7 @@ yay.create_autocmd("UpgradeSelect", {
end
end
return { exclude = exclude, skip_menu = true }
return { exclude = exclude, skip_menu = false }
end,
})
```
@@ -244,3 +244,49 @@ yay.create_autocmd("AURPreInstall", {
end,
})
```
## AUR post-download hooks
`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:
```lua
{
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
```lua
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,
})
```

View File

@@ -37,3 +37,13 @@ func luaAbortError(err error) (abortError, bool) {
return abortErr, ok
}
// wrapLuaErr strips the gopher-lua API wrapper from abort errors so callers
// see the clean abort message instead of a Lua traceback.
func wrapLuaErr(err error) error {
if abortErr, ok := luaAbortError(err); ok {
return abortErr
}
return err
}

View File

@@ -8,8 +8,9 @@ import (
)
const (
EventAURPreInstall = "AURPreInstall"
EventUpgradeSelect = "UpgradeSelect"
EventAURPreInstall = "AURPreInstall"
EventAURPostDownload = "AURPostDownload"
EventUpgradeSelect = "UpgradeSelect"
)
type Autocmd struct {
@@ -82,7 +83,7 @@ type UpgradeSelectResult struct {
func (e *Engine) createAutocmd(state *glua.LState) int {
event := state.CheckString(1)
if event != EventAURPreInstall && event != EventUpgradeSelect {
if event != EventAURPreInstall && event != EventAURPostDownload && event != EventUpgradeSelect {
state.ArgError(1, fmt.Sprintf("unsupported event %q", event))
return 0
}
@@ -128,13 +129,26 @@ func (e *Engine) RunAURPreInstall(event *AURPreInstallEvent) error {
Fn: autocmd.callback,
NRet: 0,
Protect: true,
}, e.aurPreInstallTable(event)); err != nil {
wrapped := err
if abortErr, ok := luaAbortError(err); ok {
wrapped = abortErr
}
}, e.aurEventTable(EventAURPreInstall, event)); err != nil {
return fmt.Errorf("%s %s: %w", EventAURPreInstall, event.Base, wrapLuaErr(err))
}
}
return fmt.Errorf("%s %s: %w", EventAURPreInstall, event.Base, wrapped)
return nil
}
func (e *Engine) RunAURPostDownload(event *AURPreInstallEvent) error {
if !e.HasAutocmd(EventAURPostDownload) {
return nil
}
for _, autocmd := range e.autocmds[EventAURPostDownload] {
if err := e.L.CallByParam(glua.P{
Fn: autocmd.callback,
NRet: 0,
Protect: true,
}, e.aurEventTable(EventAURPostDownload, event)); err != nil {
return fmt.Errorf("%s %s: %w", EventAURPostDownload, event.Base, wrapLuaErr(err))
}
}
@@ -159,12 +173,7 @@ func (e *Engine) RunUpgradeSelect(event *UpgradeSelectEvent) (UpgradeSelectResul
NRet: 1,
Protect: true,
}, e.upgradeSelectTable(event)); err != nil {
wrapped := err
if abortErr, ok := luaAbortError(err); ok {
wrapped = abortErr
}
return result, fmt.Errorf("%s: %w", EventUpgradeSelect, wrapped)
return result, fmt.Errorf("%s: %w", EventUpgradeSelect, wrapLuaErr(err))
}
value := e.L.Get(-1)
@@ -190,12 +199,12 @@ func (e *Engine) RunUpgradeSelect(event *UpgradeSelectEvent) (UpgradeSelectResul
return result, nil
}
func (e *Engine) aurPreInstallTable(event *AURPreInstallEvent) *glua.LTable {
func (e *Engine) aurEventTable(eventName string, event *AURPreInstallEvent) *glua.LTable {
state := e.L
eventTable := state.NewTable()
data := state.NewTable()
eventTable.RawSetString("event", glua.LString(EventAURPreInstall))
eventTable.RawSetString("event", glua.LString(eventName))
eventTable.RawSetString("match", glua.LString(event.Base))
eventTable.RawSetString("data", data)
@@ -312,18 +321,19 @@ func (e *Engine) parseUpgradeSelectResult(value glua.LValue, validExcludes mapse
return
}
name, ok := val.(glua.LString)
lname, ok := val.(glua.LString)
if !ok {
parseErr = fmt.Errorf("exclude entries must be strings")
return
}
if !validExcludes.Contains(string(name)) {
parseErr = fmt.Errorf("unknown upgrade exclusion %q", string(name))
name := string(lname)
if !validExcludes.Contains(name) {
parseErr = fmt.Errorf("unknown upgrade exclusion %q", name)
return
}
result.Exclude = append(result.Exclude, string(name))
result.Exclude = append(result.Exclude, name)
})
if parseErr != nil {
return result, parseErr

View File

@@ -65,6 +65,23 @@ func TestCreateAutocmdRegistersUpgradeSelect(t *testing.T) {
require.True(t, e.HasAutocmd(EventUpgradeSelect))
}
func TestCreateAutocmdRegistersAURPostDownload(t *testing.T) {
e := New()
defer e.Close()
require.NoError(t, e.L.DoString(`
yay.create_autocmd("AURPostDownload", {
desc = "inspect downloaded sources",
callback = function() end,
})
`))
autocmds := e.autocmds[EventAURPostDownload]
require.Len(t, autocmds, 1)
require.Equal(t, "inspect downloaded sources", autocmds[0].Desc)
require.True(t, e.HasAutocmd(EventAURPostDownload))
}
func TestCreateAutocmdRejectsInvalidEvent(t *testing.T) {
e := New()
defer e.Close()
@@ -125,6 +142,76 @@ func TestRunAURPreInstallReturnsAbortWithoutTraceback(t *testing.T) {
require.EqualError(t, err, "AURPreInstall demo-base: blocked by policy")
}
func TestRunAURPostDownloadEventTableShape(t *testing.T) {
e := New()
defer e.Close()
seen := []string{}
e.L.SetGlobal("record", e.L.NewFunction(func(L *glua.LState) int {
seen = append(seen, L.CheckString(1))
return 0
}))
require.NoError(t, e.L.DoString(`
yay.create_autocmd("AURPostDownload", {
callback = function(event)
if event.event ~= "AURPostDownload" then error("bad event") end
if event.match ~= "demo-base" then error("bad match") end
if event.data.base ~= "demo-base" then error("bad base") end
if event.data.dir ~= "/build/demo-base" then error("bad dir") end
if event.data.pkgbuild_path ~= "/build/demo-base/PKGBUILD" then error("bad pkgbuild path") end
if event.data.srcinfo_path ~= "/build/demo-base/.SRCINFO" then error("bad srcinfo path") end
if event.data.pkgbuild ~= "pkgbase=demo-base" then error("bad pkgbuild") end
if event.data.version ~= "1.0-1" then error("bad version") end
if event.data.last_modified ~= 123 then error("bad last modified") end
if event.data.installed ~= true then error("bad installed") end
if event.data.packages[1].name ~= "demo" then error("bad package") end
if event.data.srcinfo.pkgbase ~= "demo-base" then error("bad srcinfo") end
if event.data.install_paths ~= nil then error("unexpected install paths") end
if event.data.source_paths ~= nil then error("unexpected source paths") end
if event.data.sources ~= nil then error("unexpected sources") end
record(event.match .. ":" .. event.data.pkgbuild_path)
end,
})
`))
err := e.RunAURPostDownload(&AURPreInstallEvent{
Base: "demo-base",
Dir: "/build/demo-base",
PKGBUILDPath: "/build/demo-base/PKGBUILD",
SRCINFOPath: "/build/demo-base/.SRCINFO",
PKGBUILD: "pkgbase=demo-base",
Version: "1.0-1",
LastModified: 123,
Installed: true,
Packages: []AURPreInstallPackage{{
Name: "demo",
}},
SRCINFO: AURPreInstallSRCINFO{
Pkgbase: "demo-base",
},
})
require.NoError(t, err)
require.Equal(t, []string{"demo-base:/build/demo-base/PKGBUILD"}, seen)
}
func TestRunAURPostDownloadReturnsAbortWithoutTraceback(t *testing.T) {
e := New()
defer e.Close()
require.NoError(t, e.L.DoString(`
yay.create_autocmd("AURPostDownload", {
callback = function()
yay.abort("blocked by policy")
end,
})
`))
err := e.RunAURPostDownload(&AURPreInstallEvent{Base: "demo-base"})
require.EqualError(t, err, "AURPostDownload demo-base: blocked by policy")
}
func TestRunUpgradeSelectEventTableShapeAndReturn(t *testing.T) {
e := New()
defer e.Close()

View File

@@ -0,0 +1,50 @@
package workdir
import (
"github.com/Jguer/yay/v12/pkg/dep"
"github.com/Jguer/yay/v12/pkg/runtime"
settingslua "github.com/Jguer/yay/v12/pkg/settings/lua"
mapset "github.com/deckarep/golang-set/v2"
)
func runAURPostDownloadLuaHooks(run *runtime.Runtime, pkgbuildDirsByBase map[string]string,
installed mapset.Set[string], targets []map[string]*dep.InstallInfo,
) error {
if run == nil || run.Lua == nil || !run.Lua.HasAutocmd(settingslua.EventAURPostDownload) {
return nil
}
events, err := aurPostDownloadEvents(pkgbuildDirsByBase, installed, targets)
if err != nil {
return err
}
for i := range events {
if err := run.Lua.RunAURPostDownload(&events[i]); err != nil {
return err
}
}
return nil
}
func aurPostDownloadEvents(pkgbuildDirsByBase map[string]string, installed mapset.Set[string],
targets []map[string]*dep.InstallInfo,
) ([]settingslua.AURPreInstallEvent, error) {
packagesByBase := aurTargetPackagesByBase(targets)
bases := sortedAURBases(pkgbuildDirsByBase)
events := make([]settingslua.AURPreInstallEvent, 0, len(bases))
for _, base := range bases {
event, err := aurPackageEvent(settingslua.EventAURPostDownload, base, pkgbuildDirsByBase[base],
packagesByBase[base], installed, targets)
if err != nil {
return nil, err
}
events = append(events, event)
}
return events, nil
}

View File

@@ -0,0 +1,103 @@
//go:build !integration
// +build !integration
package workdir
import (
"os"
"path/filepath"
"testing"
mapset "github.com/deckarep/golang-set/v2"
"github.com/stretchr/testify/require"
glua "github.com/yuin/gopher-lua"
"github.com/Jguer/yay/v12/pkg/dep"
"github.com/Jguer/yay/v12/pkg/runtime"
settingslua "github.com/Jguer/yay/v12/pkg/settings/lua"
)
func TestAURPostDownloadEventsUseAURPreInstallPayload(t *testing.T) {
base := "demo-base"
dir := writeAURPostDownloadPackage(t, base)
events, err := aurPostDownloadEvents(map[string]string{base: dir},
mapset.NewThreadUnsafeSet[string](),
[]map[string]*dep.InstallInfo{
{
"demo": {Source: dep.AUR, AURBase: &base, Version: "1.0-1"},
},
})
require.NoError(t, err)
require.Len(t, events, 1)
event := events[0]
require.Equal(t, base, event.Base)
require.Equal(t, dir, event.Dir)
require.Equal(t, filepath.Join(dir, "PKGBUILD"), event.PKGBUILDPath)
require.Equal(t, filepath.Join(dir, ".SRCINFO"), event.SRCINFOPath)
require.Contains(t, event.PKGBUILD, "pkgbase=demo-base")
require.Equal(t, "1.0-1", event.Version)
require.Equal(t, base, event.SRCINFO.Pkgbase)
require.Equal(t, "1.0", event.SRCINFO.Pkgver)
require.Equal(t, "1", event.SRCINFO.Pkgrel)
require.Equal(t, "1.0-1", event.SRCINFO.Version)
require.Equal(t, []string{"any"}, event.SRCINFO.Arch)
require.Equal(t, []settingslua.AURPreInstallPackage{{Name: "demo", Version: "1.0-1", Reason: "explicit"}}, event.Packages)
}
func TestRunAURPostDownloadLuaHooksRunsBasesInSortedOrder(t *testing.T) {
firstDir := writeAURPostDownloadPackage(t, "a-base")
secondDir := writeAURPostDownloadPackage(t, "z-base")
engine := settingslua.New()
defer engine.Close()
order := []string{}
engine.L.SetGlobal("record", engine.L.NewFunction(func(L *glua.LState) int {
order = append(order, L.CheckString(1))
return 0
}))
require.NoError(t, engine.L.DoString(`
yay.create_autocmd("AURPostDownload", {
callback = function(event)
record(event.match .. ":" .. event.data.pkgbuild_path)
end,
})
`))
err := runAURPostDownloadLuaHooks(&runtime.Runtime{Lua: engine},
map[string]string{"z-base": secondDir, "a-base": firstDir},
mapset.NewThreadUnsafeSet[string](),
[]map[string]*dep.InstallInfo{
{
"a": {Source: dep.AUR, AURBase: ptrString("a-base")},
"z": {Source: dep.AUR, AURBase: ptrString("z-base")},
},
})
require.NoError(t, err)
require.Equal(t, []string{
"a-base:" + filepath.Join(firstDir, "PKGBUILD"),
"z-base:" + filepath.Join(secondDir, "PKGBUILD"),
}, order)
}
func writeAURPostDownloadPackage(t *testing.T, base string) string {
t.Helper()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "PKGBUILD"), []byte(`pkgbase=`+base+`
pkgname=(demo)
pkgver=1.0
pkgrel=1
`), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(dir, ".SRCINFO"), []byte(`pkgbase = `+base+`
pkgver = 1.0
pkgrel = 1
arch = any
pkgname = demo
`), 0o600))
return dir
}

View File

@@ -90,19 +90,25 @@ func sortedAURBases(pkgbuildDirsByBase map[string]string) []string {
func aurPreInstallEvent(base, path string, packages []settingslua.AURPreInstallPackage,
installed mapset.Set[string], targets []map[string]*dep.InstallInfo,
) (settingslua.AURPreInstallEvent, error) {
return aurPackageEvent(settingslua.EventAURPreInstall, base, path, packages, installed, targets)
}
func aurPackageEvent(eventName, base, path string, packages []settingslua.AURPreInstallPackage,
installed mapset.Set[string], targets []map[string]*dep.InstallInfo,
) (settingslua.AURPreInstallEvent, error) {
dir, pkgbuildPath, srcinfoPath := aurPreInstallPaths(path)
pkgbuildBytes, err := os.ReadFile(pkgbuildPath)
if err != nil {
return settingslua.AURPreInstallEvent{},
fmt.Errorf("%s %s: read PKGBUILD: %w", settingslua.EventAURPreInstall, base, err)
fmt.Errorf("%s %s: read PKGBUILD: %w", eventName, base, err)
}
srcinfo, err := gosrc.ParseFile(srcinfoPath)
if err != nil {
return settingslua.AURPreInstallEvent{},
fmt.Errorf("%s %s: parse .SRCINFO: %w", settingslua.EventAURPreInstall, base, err)
fmt.Errorf("%s %s: parse .SRCINFO: %w", eventName, base, err)
}
if len(packages) == 0 {

View File

@@ -235,6 +235,10 @@ func (preper *Preparer) PrepareWorkspace(ctx context.Context,
preper.log.Errorln(errP)
}
if err := runAURPostDownloadLuaHooks(run, pkgBuildDirsByBase, remoteNamesCache, targets); err != nil {
return nil, err
}
return pkgBuildDirsByBase, nil
}