Skip to content

Commit 16479dc

Browse files
authored
feat: Add AURPostDownload hook (#2856)
* run hooks after source download * lint * nitpick: remove event table indirection
1 parent 421b65b commit 16479dc

9 files changed

Lines changed: 353 additions & 25 deletions

File tree

doc/init.lua

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ yay.opt.double_confirm = true -- Ask for confirmation before and after builds du
7373
-- end
7474
-- end
7575
--
76-
-- return { exclude = exclude, skip_menu = true }
76+
-- return { exclude = exclude, skip_menu = false }
7777
-- end,
7878
-- })
7979
--
@@ -95,3 +95,15 @@ yay.opt.double_confirm = true -- Ask for confirmation before and after builds du
9595
-- -- f:close()
9696
-- end,
9797
-- })
98+
--
99+
-- Run Lua after yay downloads/verifies package sources and before builds or
100+
-- installs. AURPostDownload receives the same payload shape as AURPreInstall.
101+
--
102+
-- yay.create_autocmd("AURPostDownload", {
103+
-- desc = "block forbidden source URLs after download",
104+
-- callback = function(event)
105+
-- if event.data.pkgbuild:match("forbidden.example") then
106+
-- yay.abort(event.match .. ": forbidden source URL")
107+
-- end
108+
-- end,
109+
-- })

doc/lua.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ yay.create_autocmd("UpgradeSelect", {
8383
end
8484
end
8585

86-
return { exclude = exclude, skip_menu = true }
86+
return { exclude = exclude, skip_menu = false }
8787
end,
8888
})
8989
```
@@ -244,3 +244,49 @@ yay.create_autocmd("AURPreInstall", {
244244
end,
245245
})
246246
```
247+
248+
## AUR post-download hooks
249+
250+
`AURPostDownload` runs once per AUR package base, in sorted package-base order,
251+
after yay runs `makepkg --verifysource` for package sources and before
252+
compatibility checks, PGP key import prompts, builds, or package installs.
253+
254+
Use `yay.abort("message")` to stop the operation without a Lua traceback.
255+
`AURPostDownload` receives the same payload shape as `AURPreInstall`; only the
256+
`event` value differs.
257+
258+
### AURPostDownload event
259+
260+
The callback receives this table:
261+
262+
```lua
263+
{
264+
event = "AURPostDownload",
265+
match = "pkgbase",
266+
data = {
267+
base = "pkgbase",
268+
dir = "/path/to/build/pkgbase",
269+
pkgbuild_path = "/path/to/build/pkgbase/PKGBUILD",
270+
srcinfo_path = "/path/to/build/pkgbase/.SRCINFO",
271+
pkgbuild = "...PKGBUILD contents...",
272+
version = "1:1.2.3-4",
273+
last_modified = 1700000000,
274+
installed = true,
275+
packages = { ... },
276+
srcinfo = { ... },
277+
},
278+
}
279+
```
280+
281+
### Example
282+
283+
```lua
284+
yay.create_autocmd("AURPostDownload", {
285+
desc = "block forbidden source URLs after download",
286+
callback = function(event)
287+
if event.data.pkgbuild:match("forbidden.example") then
288+
yay.abort(event.match .. ": forbidden source URL")
289+
end
290+
end,
291+
})
292+
```

pkg/settings/lua/abort.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,13 @@ func luaAbortError(err error) (abortError, bool) {
3737

3838
return abortErr, ok
3939
}
40+
41+
// wrapLuaErr strips the gopher-lua API wrapper from abort errors so callers
42+
// see the clean abort message instead of a Lua traceback.
43+
func wrapLuaErr(err error) error {
44+
if abortErr, ok := luaAbortError(err); ok {
45+
return abortErr
46+
}
47+
48+
return err
49+
}

pkg/settings/lua/autocmd.go

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import (
88
)
99

1010
const (
11-
EventAURPreInstall = "AURPreInstall"
12-
EventUpgradeSelect = "UpgradeSelect"
11+
EventAURPreInstall = "AURPreInstall"
12+
EventAURPostDownload = "AURPostDownload"
13+
EventUpgradeSelect = "UpgradeSelect"
1314
)
1415

1516
type Autocmd struct {
@@ -82,7 +83,7 @@ type UpgradeSelectResult struct {
8283

8384
func (e *Engine) createAutocmd(state *glua.LState) int {
8485
event := state.CheckString(1)
85-
if event != EventAURPreInstall && event != EventUpgradeSelect {
86+
if event != EventAURPreInstall && event != EventAURPostDownload && event != EventUpgradeSelect {
8687
state.ArgError(1, fmt.Sprintf("unsupported event %q", event))
8788
return 0
8889
}
@@ -128,13 +129,26 @@ func (e *Engine) RunAURPreInstall(event *AURPreInstallEvent) error {
128129
Fn: autocmd.callback,
129130
NRet: 0,
130131
Protect: true,
131-
}, e.aurPreInstallTable(event)); err != nil {
132-
wrapped := err
133-
if abortErr, ok := luaAbortError(err); ok {
134-
wrapped = abortErr
135-
}
132+
}, e.aurEventTable(EventAURPreInstall, event)); err != nil {
133+
return fmt.Errorf("%s %s: %w", EventAURPreInstall, event.Base, wrapLuaErr(err))
134+
}
135+
}
136+
137+
return nil
138+
}
139+
140+
func (e *Engine) RunAURPostDownload(event *AURPreInstallEvent) error {
141+
if !e.HasAutocmd(EventAURPostDownload) {
142+
return nil
143+
}
136144

137-
return fmt.Errorf("%s %s: %w", EventAURPreInstall, event.Base, wrapped)
145+
for _, autocmd := range e.autocmds[EventAURPostDownload] {
146+
if err := e.L.CallByParam(glua.P{
147+
Fn: autocmd.callback,
148+
NRet: 0,
149+
Protect: true,
150+
}, e.aurEventTable(EventAURPostDownload, event)); err != nil {
151+
return fmt.Errorf("%s %s: %w", EventAURPostDownload, event.Base, wrapLuaErr(err))
138152
}
139153
}
140154

@@ -159,12 +173,7 @@ func (e *Engine) RunUpgradeSelect(event *UpgradeSelectEvent) (UpgradeSelectResul
159173
NRet: 1,
160174
Protect: true,
161175
}, e.upgradeSelectTable(event)); err != nil {
162-
wrapped := err
163-
if abortErr, ok := luaAbortError(err); ok {
164-
wrapped = abortErr
165-
}
166-
167-
return result, fmt.Errorf("%s: %w", EventUpgradeSelect, wrapped)
176+
return result, fmt.Errorf("%s: %w", EventUpgradeSelect, wrapLuaErr(err))
168177
}
169178

170179
value := e.L.Get(-1)
@@ -190,12 +199,12 @@ func (e *Engine) RunUpgradeSelect(event *UpgradeSelectEvent) (UpgradeSelectResul
190199
return result, nil
191200
}
192201

193-
func (e *Engine) aurPreInstallTable(event *AURPreInstallEvent) *glua.LTable {
202+
func (e *Engine) aurEventTable(eventName string, event *AURPreInstallEvent) *glua.LTable {
194203
state := e.L
195204
eventTable := state.NewTable()
196205
data := state.NewTable()
197206

198-
eventTable.RawSetString("event", glua.LString(EventAURPreInstall))
207+
eventTable.RawSetString("event", glua.LString(eventName))
199208
eventTable.RawSetString("match", glua.LString(event.Base))
200209
eventTable.RawSetString("data", data)
201210

@@ -312,18 +321,19 @@ func (e *Engine) parseUpgradeSelectResult(value glua.LValue, validExcludes mapse
312321
return
313322
}
314323

315-
name, ok := val.(glua.LString)
324+
lname, ok := val.(glua.LString)
316325
if !ok {
317326
parseErr = fmt.Errorf("exclude entries must be strings")
318327
return
319328
}
320329

321-
if !validExcludes.Contains(string(name)) {
322-
parseErr = fmt.Errorf("unknown upgrade exclusion %q", string(name))
330+
name := string(lname)
331+
if !validExcludes.Contains(name) {
332+
parseErr = fmt.Errorf("unknown upgrade exclusion %q", name)
323333
return
324334
}
325335

326-
result.Exclude = append(result.Exclude, string(name))
336+
result.Exclude = append(result.Exclude, name)
327337
})
328338
if parseErr != nil {
329339
return result, parseErr

pkg/settings/lua/autocmd_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,23 @@ func TestCreateAutocmdRegistersUpgradeSelect(t *testing.T) {
6565
require.True(t, e.HasAutocmd(EventUpgradeSelect))
6666
}
6767

68+
func TestCreateAutocmdRegistersAURPostDownload(t *testing.T) {
69+
e := New()
70+
defer e.Close()
71+
72+
require.NoError(t, e.L.DoString(`
73+
yay.create_autocmd("AURPostDownload", {
74+
desc = "inspect downloaded sources",
75+
callback = function() end,
76+
})
77+
`))
78+
79+
autocmds := e.autocmds[EventAURPostDownload]
80+
require.Len(t, autocmds, 1)
81+
require.Equal(t, "inspect downloaded sources", autocmds[0].Desc)
82+
require.True(t, e.HasAutocmd(EventAURPostDownload))
83+
}
84+
6885
func TestCreateAutocmdRejectsInvalidEvent(t *testing.T) {
6986
e := New()
7087
defer e.Close()
@@ -125,6 +142,76 @@ func TestRunAURPreInstallReturnsAbortWithoutTraceback(t *testing.T) {
125142
require.EqualError(t, err, "AURPreInstall demo-base: blocked by policy")
126143
}
127144

145+
func TestRunAURPostDownloadEventTableShape(t *testing.T) {
146+
e := New()
147+
defer e.Close()
148+
149+
seen := []string{}
150+
e.L.SetGlobal("record", e.L.NewFunction(func(L *glua.LState) int {
151+
seen = append(seen, L.CheckString(1))
152+
return 0
153+
}))
154+
155+
require.NoError(t, e.L.DoString(`
156+
yay.create_autocmd("AURPostDownload", {
157+
callback = function(event)
158+
if event.event ~= "AURPostDownload" then error("bad event") end
159+
if event.match ~= "demo-base" then error("bad match") end
160+
if event.data.base ~= "demo-base" then error("bad base") end
161+
if event.data.dir ~= "/build/demo-base" then error("bad dir") end
162+
if event.data.pkgbuild_path ~= "/build/demo-base/PKGBUILD" then error("bad pkgbuild path") end
163+
if event.data.srcinfo_path ~= "/build/demo-base/.SRCINFO" then error("bad srcinfo path") end
164+
if event.data.pkgbuild ~= "pkgbase=demo-base" then error("bad pkgbuild") end
165+
if event.data.version ~= "1.0-1" then error("bad version") end
166+
if event.data.last_modified ~= 123 then error("bad last modified") end
167+
if event.data.installed ~= true then error("bad installed") end
168+
if event.data.packages[1].name ~= "demo" then error("bad package") end
169+
if event.data.srcinfo.pkgbase ~= "demo-base" then error("bad srcinfo") end
170+
if event.data.install_paths ~= nil then error("unexpected install paths") end
171+
if event.data.source_paths ~= nil then error("unexpected source paths") end
172+
if event.data.sources ~= nil then error("unexpected sources") end
173+
174+
record(event.match .. ":" .. event.data.pkgbuild_path)
175+
end,
176+
})
177+
`))
178+
179+
err := e.RunAURPostDownload(&AURPreInstallEvent{
180+
Base: "demo-base",
181+
Dir: "/build/demo-base",
182+
PKGBUILDPath: "/build/demo-base/PKGBUILD",
183+
SRCINFOPath: "/build/demo-base/.SRCINFO",
184+
PKGBUILD: "pkgbase=demo-base",
185+
Version: "1.0-1",
186+
LastModified: 123,
187+
Installed: true,
188+
Packages: []AURPreInstallPackage{{
189+
Name: "demo",
190+
}},
191+
SRCINFO: AURPreInstallSRCINFO{
192+
Pkgbase: "demo-base",
193+
},
194+
})
195+
require.NoError(t, err)
196+
require.Equal(t, []string{"demo-base:/build/demo-base/PKGBUILD"}, seen)
197+
}
198+
199+
func TestRunAURPostDownloadReturnsAbortWithoutTraceback(t *testing.T) {
200+
e := New()
201+
defer e.Close()
202+
203+
require.NoError(t, e.L.DoString(`
204+
yay.create_autocmd("AURPostDownload", {
205+
callback = function()
206+
yay.abort("blocked by policy")
207+
end,
208+
})
209+
`))
210+
211+
err := e.RunAURPostDownload(&AURPreInstallEvent{Base: "demo-base"})
212+
require.EqualError(t, err, "AURPostDownload demo-base: blocked by policy")
213+
}
214+
128215
func TestRunUpgradeSelectEventTableShapeAndReturn(t *testing.T) {
129216
e := New()
130217
defer e.Close()
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package workdir
2+
3+
import (
4+
"github.com/Jguer/yay/v12/pkg/dep"
5+
"github.com/Jguer/yay/v12/pkg/runtime"
6+
settingslua "github.com/Jguer/yay/v12/pkg/settings/lua"
7+
8+
mapset "github.com/deckarep/golang-set/v2"
9+
)
10+
11+
func runAURPostDownloadLuaHooks(run *runtime.Runtime, pkgbuildDirsByBase map[string]string,
12+
installed mapset.Set[string], targets []map[string]*dep.InstallInfo,
13+
) error {
14+
if run == nil || run.Lua == nil || !run.Lua.HasAutocmd(settingslua.EventAURPostDownload) {
15+
return nil
16+
}
17+
18+
events, err := aurPostDownloadEvents(pkgbuildDirsByBase, installed, targets)
19+
if err != nil {
20+
return err
21+
}
22+
23+
for i := range events {
24+
if err := run.Lua.RunAURPostDownload(&events[i]); err != nil {
25+
return err
26+
}
27+
}
28+
29+
return nil
30+
}
31+
32+
func aurPostDownloadEvents(pkgbuildDirsByBase map[string]string, installed mapset.Set[string],
33+
targets []map[string]*dep.InstallInfo,
34+
) ([]settingslua.AURPreInstallEvent, error) {
35+
packagesByBase := aurTargetPackagesByBase(targets)
36+
bases := sortedAURBases(pkgbuildDirsByBase)
37+
38+
events := make([]settingslua.AURPreInstallEvent, 0, len(bases))
39+
for _, base := range bases {
40+
event, err := aurPackageEvent(settingslua.EventAURPostDownload, base, pkgbuildDirsByBase[base],
41+
packagesByBase[base], installed, targets)
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
events = append(events, event)
47+
}
48+
49+
return events, nil
50+
}

0 commit comments

Comments
 (0)