feat(lua): add search filter and post install hooks (#2867)
* search filter and post install add examples * add tag
This commit is contained in:
12 files changed
+1010
-34
No files matched your search
@@ -0,0 +1,16 @@
|
||||
yay.create_autocmd("SearchFilter", {
|
||||
desc = "hide AUR packages submitted in the last 14 days",
|
||||
callback = function(event)
|
||||
yay.log.info("hiding AUR packages submitted in the last 14 days")
|
||||
local out = {}
|
||||
local cutoff = os.time() - (14 * 24 * 60 * 60)
|
||||
for _, r in ipairs(event.data.results) do
|
||||
if r.source == "aur" and r.first_submitted ~= -1 and r.first_submitted >= cutoff then
|
||||
yay.log.debug("hiding newly submitted AUR package: ", r.name)
|
||||
else
|
||||
out[#out + 1] = { source = r.source, name = r.name }
|
||||
end
|
||||
end
|
||||
return out
|
||||
end,
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
local log_path = (os.getenv("XDG_STATE_HOME") or (os.getenv("HOME") .. "/.local/state")) .. "/yay/install.log"
|
||||
local log_dir = log_path:match("^(.+)/[^/]+$")
|
||||
|
||||
yay.create_autocmd("PostInstall", {
|
||||
desc = "append every installed/upgraded package to a persistent log",
|
||||
callback = function(event)
|
||||
yay.log.info("install_log: writing to ", log_path)
|
||||
os.execute("mkdir -p " .. log_dir)
|
||||
local f, err = io.open(log_path, "a")
|
||||
if not f then
|
||||
yay.log.warn("install_log: cannot open log file: ", err)
|
||||
return
|
||||
end
|
||||
|
||||
local ts = os.date("%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
for _, pkg in ipairs(event.data.packages) do
|
||||
if pkg.installed then
|
||||
local action = pkg.upgrade and "upgrade" or "install"
|
||||
local version_change
|
||||
if pkg.upgrade then
|
||||
version_change = pkg.local_version .. " -> " .. pkg.version
|
||||
else
|
||||
version_change = pkg.version
|
||||
end
|
||||
|
||||
local flags = pkg.devel and " devel" or ""
|
||||
f:write(string.format("%s %-9s %-7s %-14s %-12s %s%s\n",
|
||||
ts, action, pkg.source, pkg.reason, pkg.name, version_change, flags))
|
||||
end
|
||||
end
|
||||
|
||||
f:close()
|
||||
end,
|
||||
})
|
||||
@@ -103,3 +103,33 @@ yay.opt.double_confirm = true -- Ask for confirmation before and after builds du
|
||||
-- end
|
||||
-- end,
|
||||
-- })
|
||||
--
|
||||
-- Run Lua once after a successful install/upgrade transaction (skipped on
|
||||
-- --downloadonly). The callback is fire-and-forget; returning has no effect.
|
||||
--
|
||||
-- yay.create_autocmd("PostInstall", {
|
||||
-- desc = "log every package yay installed",
|
||||
-- callback = function(event)
|
||||
-- for _, pkg in ipairs(event.data.packages) do
|
||||
-- if pkg.installed then
|
||||
-- yay.log.info(pkg.name .. " " .. pkg.version .. " installed (" .. pkg.source .. ")")
|
||||
-- end
|
||||
-- end
|
||||
-- end,
|
||||
-- })
|
||||
--
|
||||
-- Run Lua during -Ss / -S number menu after ranking, before display. Return
|
||||
-- an ordered array of {source=, name=} to filter/reorder; nil = unchanged.
|
||||
--
|
||||
-- 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,
|
||||
-- })
|
||||
+114
@@ -302,3 +302,117 @@ yay.create_autocmd("AURPostDownload", {
|
||||
end,
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Post-install hooks
|
||||
|
||||
<p class="api-since">Available from yay v13.0.0</p>
|
||||
|
||||
`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
|
||||
|
||||
```lua
|
||||
{
|
||||
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"
|
||||
installed = true, -- false for AUR bases that failed but were tolerated
|
||||
upgrade = false, -- true when replacing an older version
|
||||
devel = false, -- true for VCS (-git/-svn/…) packages
|
||||
},
|
||||
-- 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.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
yay.create_autocmd("PostInstall", {
|
||||
desc = "log every package yay installed",
|
||||
callback = function(event)
|
||||
for _, pkg in ipairs(event.data.packages) do
|
||||
if pkg.installed then
|
||||
yay.log.info(pkg.name .. " " .. pkg.version .. " installed (" .. pkg.source .. ")")
|
||||
end
|
||||
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
|
||||
|
||||
<p class="api-since">Available from yay v13.0.0</p>
|
||||
|
||||
```lua
|
||||
{
|
||||
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
|
||||
provides = { "virtual-pkg" },
|
||||
},
|
||||
-- …
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
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,
|
||||
})
|
||||
```
|
||||
@@ -133,6 +133,7 @@ func main() {
|
||||
luaEngine.SetLogger(run.Logger.Child("lua"))
|
||||
}
|
||||
run.Lua = luaEngine
|
||||
run.QueryBuilder.SetLua(luaEngine)
|
||||
|
||||
dbExecutor, err := ialpm.NewExecutor(run.PacmanConf, run.Logger.Child("db"))
|
||||
if err != nil {
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/Jguer/yay/v12/pkg/db"
|
||||
"github.com/Jguer/yay/v12/pkg/intrange"
|
||||
settingslua "github.com/Jguer/yay/v12/pkg/settings/lua"
|
||||
"github.com/Jguer/yay/v12/pkg/settings/parser"
|
||||
"github.com/Jguer/yay/v12/pkg/text"
|
||||
)
|
||||
@@ -35,6 +36,7 @@ type Builder interface {
|
||||
Execute(ctx context.Context, dbExecutor db.Executor, pkgS []string)
|
||||
Results(dbExecutor db.Executor, verboseSearch SearchVerbosity) error
|
||||
GetTargets(include, exclude intrange.IntRanges, otherExclude mapset.Set[string]) ([]string, error)
|
||||
SetLua(engine *settingslua.Engine)
|
||||
}
|
||||
|
||||
type SortFunc func(pkgA, pkgB abstractResult) int
|
||||
@@ -51,6 +53,7 @@ type SourceQueryBuilder struct {
|
||||
|
||||
aurClient aur.QueryClient
|
||||
logger *text.Logger
|
||||
lua *settingslua.Engine
|
||||
}
|
||||
|
||||
func NewSourceQueryBuilder(
|
||||
@@ -77,6 +80,10 @@ func NewSourceQueryBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SourceQueryBuilder) SetLua(engine *settingslua.Engine) {
|
||||
s.lua = engine
|
||||
}
|
||||
|
||||
type abstractResult struct {
|
||||
source string
|
||||
name string
|
||||
@@ -262,7 +269,7 @@ func (s *SourceQueryBuilder) Execute(ctx context.Context, dbExecutor db.Executor
|
||||
}
|
||||
|
||||
sort.Sort(sortableResults)
|
||||
s.results = sortableResults.results
|
||||
s.results = s.applySearchFilter(sortableResults.results)
|
||||
|
||||
if aurErr != nil {
|
||||
s.logger.Errorln(ErrAURSearch{inner: aurErr})
|
||||
@@ -353,3 +360,46 @@ func matchesSearch(pkg *aur.Pkg, terms []string) bool {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *SourceQueryBuilder) applySearchFilter(results []abstractResult) []abstractResult {
|
||||
if s.lua == nil || !s.lua.HasAutocmd(settingslua.EventSearchFilter) {
|
||||
return results
|
||||
}
|
||||
|
||||
pkgs := make([]settingslua.SearchResultPackage, len(results))
|
||||
for i := range results {
|
||||
pkgs[i] = settingslua.SearchResultPackage{
|
||||
Source: results[i].source,
|
||||
Name: results[i].name,
|
||||
Description: results[i].description,
|
||||
Base: results[i].packageBase,
|
||||
Votes: results[i].votes,
|
||||
Popularity: results[i].popularity,
|
||||
FirstSubmitted: results[i].firstSubmitted,
|
||||
LastModified: results[i].lastModified,
|
||||
Provides: results[i].provides,
|
||||
}
|
||||
}
|
||||
|
||||
refs, err := s.lua.RunSearchFilter(&settingslua.SearchFilterEvent{Results: pkgs})
|
||||
if err != nil {
|
||||
s.logger.Errorln(err)
|
||||
return results
|
||||
}
|
||||
|
||||
if refs == nil {
|
||||
return results
|
||||
}
|
||||
|
||||
byRef := make(map[settingslua.SearchResultRef]abstractResult, len(results))
|
||||
for i := range results {
|
||||
byRef[settingslua.SearchResultRef{Source: results[i].source, Name: results[i].name}] = results[i]
|
||||
}
|
||||
|
||||
filtered := make([]abstractResult, 0, len(refs))
|
||||
for _, ref := range refs {
|
||||
filtered = append(filtered, byRef[ref])
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//go:build !integration
|
||||
// +build !integration
|
||||
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
settingslua "github.com/Jguer/yay/v12/pkg/settings/lua"
|
||||
"github.com/Jguer/yay/v12/pkg/settings/parser"
|
||||
"github.com/Jguer/yay/v12/pkg/text"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestSourceQueryBuilderSearchFilterHook verifies that a SearchFilter hook
|
||||
// registered on the Lua engine is applied after ranking, filtering s.results
|
||||
// to only the entries the callback returns.
|
||||
func TestSourceQueryBuilderSearchFilterHook(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockDB, mockAUR := newYayQueryBuilderMocks()
|
||||
|
||||
w := &strings.Builder{}
|
||||
logger := text.NewLogger(w, io.Discard, strings.NewReader(""), false, "test")
|
||||
|
||||
// Baseline: build without a Lua hook and record count.
|
||||
baselineQB := NewSourceQueryBuilder(mockAUR, logger, "", parser.ModeAny, "", false, false, false)
|
||||
baselineQB.Execute(context.Background(), mockDB, []string{"yay"})
|
||||
baselineCount := len(baselineQB.results)
|
||||
require.Greater(t, baselineCount, 0, "baseline query must return at least one result")
|
||||
|
||||
// Count how many AUR results exist in the baseline.
|
||||
aurCount := 0
|
||||
for _, r := range baselineQB.results {
|
||||
if r.source == "aur" {
|
||||
aurCount++
|
||||
}
|
||||
}
|
||||
require.Greater(t, aurCount, 0, "baseline must have at least one AUR result")
|
||||
|
||||
// The mock also returns a non-AUR (sync) result for "yay".
|
||||
// Confirm there is at least one so the filter actually does something.
|
||||
nonAURCount := baselineCount - aurCount
|
||||
require.Greater(t, nonAURCount, 0, "baseline must have at least one non-AUR result for the filter to exercise")
|
||||
|
||||
// Build with a SearchFilter that keeps only AUR results.
|
||||
e := settingslua.New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("SearchFilter", {
|
||||
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,
|
||||
})
|
||||
`))
|
||||
|
||||
filteredQB := NewSourceQueryBuilder(mockAUR, logger, "", parser.ModeAny, "", false, false, false)
|
||||
filteredQB.SetLua(e)
|
||||
filteredQB.Execute(context.Background(), mockDB, []string{"yay"})
|
||||
|
||||
assert.Equal(t, aurCount, len(filteredQB.results),
|
||||
"filtered results should equal the number of AUR packages in the baseline")
|
||||
|
||||
for _, r := range filteredQB.results {
|
||||
assert.Equal(t, "aur", r.source,
|
||||
"every result after the SearchFilter hook must be from aur")
|
||||
}
|
||||
}
|
||||
+242
-33
@@ -11,6 +11,8 @@ const (
|
||||
EventAURPreInstall = "AURPreInstall"
|
||||
EventAURPostDownload = "AURPostDownload"
|
||||
EventUpgradeSelect = "UpgradeSelect"
|
||||
EventPostInstall = "PostInstall"
|
||||
EventSearchFilter = "SearchFilter"
|
||||
)
|
||||
|
||||
type Autocmd struct {
|
||||
@@ -82,9 +84,46 @@ type UpgradeSelectResult struct {
|
||||
SkipMenu bool
|
||||
}
|
||||
|
||||
type PostInstallEvent struct {
|
||||
Packages []PostInstallPackage
|
||||
}
|
||||
|
||||
type PostInstallPackage struct {
|
||||
Name string
|
||||
Version string
|
||||
LocalVersion string
|
||||
Source string
|
||||
Reason string
|
||||
Installed bool
|
||||
Upgrade bool
|
||||
Devel bool
|
||||
}
|
||||
|
||||
type SearchFilterEvent struct {
|
||||
Results []SearchResultPackage
|
||||
}
|
||||
|
||||
type SearchResultPackage struct {
|
||||
Source string
|
||||
Name string
|
||||
Description string
|
||||
Base string
|
||||
Votes int
|
||||
Popularity float64
|
||||
FirstSubmitted int
|
||||
LastModified int
|
||||
Provides []string
|
||||
}
|
||||
|
||||
type SearchResultRef struct {
|
||||
Source string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (e *Engine) createAutocmd(state *glua.LState) int {
|
||||
event := state.CheckString(1)
|
||||
if event != EventAURPreInstall && event != EventAURPostDownload && event != EventUpgradeSelect {
|
||||
if event != EventAURPreInstall && event != EventAURPostDownload &&
|
||||
event != EventUpgradeSelect && event != EventPostInstall && event != EventSearchFilter {
|
||||
state.ArgError(1, fmt.Sprintf("unsupported event %q", event))
|
||||
return 0
|
||||
}
|
||||
@@ -121,35 +160,25 @@ func (e *Engine) HasAutocmd(event string) bool {
|
||||
}
|
||||
|
||||
func (e *Engine) RunAURPreInstall(event *AURPreInstallEvent) error {
|
||||
if !e.HasAutocmd(EventAURPreInstall) {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, autocmd := range e.autocmds[EventAURPreInstall] {
|
||||
if err := e.L.CallByParam(glua.P{
|
||||
Fn: autocmd.callback,
|
||||
NRet: 0,
|
||||
Protect: true,
|
||||
}, e.aurEventTable(EventAURPreInstall, event)); err != nil {
|
||||
return fmt.Errorf("%s %s: %w", EventAURPreInstall, event.Base, wrapLuaErr(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return e.runAUREvent(EventAURPreInstall, event)
|
||||
}
|
||||
|
||||
func (e *Engine) RunAURPostDownload(event *AURPreInstallEvent) error {
|
||||
if !e.HasAutocmd(EventAURPostDownload) {
|
||||
return e.runAUREvent(EventAURPostDownload, event)
|
||||
}
|
||||
|
||||
func (e *Engine) runAUREvent(eventName string, event *AURPreInstallEvent) error {
|
||||
if !e.HasAutocmd(eventName) {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, autocmd := range e.autocmds[EventAURPostDownload] {
|
||||
for _, autocmd := range e.autocmds[eventName] {
|
||||
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))
|
||||
}, e.aurEventTable(eventName, event)); err != nil {
|
||||
return fmt.Errorf("%s %s: %w", eventName, event.Base, wrapLuaErr(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,15 +229,19 @@ func (e *Engine) RunUpgradeSelect(event *UpgradeSelectEvent) (UpgradeSelectResul
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *Engine) aurEventTable(eventName string, event *AURPreInstallEvent) *glua.LTable {
|
||||
state := e.L
|
||||
eventTable := state.NewTable()
|
||||
data := state.NewTable()
|
||||
|
||||
func (e *Engine) newEventTable(eventName string) (eventTable, data *glua.LTable) {
|
||||
eventTable = e.L.NewTable()
|
||||
data = e.L.NewTable()
|
||||
eventTable.RawSetString("event", glua.LString(eventName))
|
||||
eventTable.RawSetString("match", glua.LString(event.Base))
|
||||
eventTable.RawSetString("data", data)
|
||||
|
||||
return eventTable, data
|
||||
}
|
||||
|
||||
func (e *Engine) aurEventTable(eventName string, event *AURPreInstallEvent) *glua.LTable {
|
||||
eventTable, data := e.newEventTable(eventName)
|
||||
eventTable.RawSetString("match", glua.LString(event.Base))
|
||||
|
||||
data.RawSetString("base", glua.LString(event.Base))
|
||||
data.RawSetString("dir", glua.LString(event.Dir))
|
||||
data.RawSetString("pkgbuild_path", glua.LString(event.PKGBUILDPath))
|
||||
@@ -224,13 +257,7 @@ func (e *Engine) aurEventTable(eventName string, event *AURPreInstallEvent) *glu
|
||||
}
|
||||
|
||||
func (e *Engine) upgradeSelectTable(event *UpgradeSelectEvent) *glua.LTable {
|
||||
state := e.L
|
||||
eventTable := state.NewTable()
|
||||
data := state.NewTable()
|
||||
|
||||
eventTable.RawSetString("event", glua.LString(EventUpgradeSelect))
|
||||
eventTable.RawSetString("data", data)
|
||||
|
||||
eventTable, data := e.newEventTable(EventUpgradeSelect)
|
||||
data.RawSetString("upgrades", e.upgradeSelectPackagesTable(event.Upgrades))
|
||||
data.RawSetString("pulled_dependencies", e.upgradeSelectPackagesTable(event.PulledDependencies))
|
||||
|
||||
@@ -363,3 +390,185 @@ func (e *Engine) stringArray(values []string) *glua.LTable {
|
||||
|
||||
return tbl
|
||||
}
|
||||
|
||||
func (e *Engine) RunPostInstall(event *PostInstallEvent) error {
|
||||
if !e.HasAutocmd(EventPostInstall) {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, autocmd := range e.autocmds[EventPostInstall] {
|
||||
if err := e.L.CallByParam(glua.P{
|
||||
Fn: autocmd.callback,
|
||||
NRet: 0,
|
||||
Protect: true,
|
||||
}, e.postInstallTable(event)); err != nil {
|
||||
return fmt.Errorf("%s: %w", EventPostInstall, wrapLuaErr(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) RunSearchFilter(event *SearchFilterEvent) ([]SearchResultRef, error) {
|
||||
if !e.HasAutocmd(EventSearchFilter) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
indexByRef := make(map[SearchResultRef]int, len(event.Results))
|
||||
for i, pkg := range event.Results {
|
||||
indexByRef[SearchResultRef{Source: pkg.Source, Name: pkg.Name}] = i
|
||||
}
|
||||
|
||||
active := event.Results
|
||||
|
||||
for _, autocmd := range e.autocmds[EventSearchFilter] {
|
||||
if err := e.L.CallByParam(glua.P{
|
||||
Fn: autocmd.callback,
|
||||
NRet: 1,
|
||||
Protect: true,
|
||||
}, e.searchFilterTable(active)); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", EventSearchFilter, wrapLuaErr(err))
|
||||
}
|
||||
|
||||
value := e.L.Get(-1)
|
||||
e.L.Pop(1)
|
||||
|
||||
refs, returned, err := parseSearchFilterResult(value, indexByRef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", EventSearchFilter, err)
|
||||
}
|
||||
|
||||
if !returned {
|
||||
continue
|
||||
}
|
||||
|
||||
next := make([]SearchResultPackage, 0, len(refs))
|
||||
for _, ref := range refs {
|
||||
next = append(next, event.Results[indexByRef[ref]])
|
||||
}
|
||||
|
||||
active = next
|
||||
}
|
||||
|
||||
result := make([]SearchResultRef, len(active))
|
||||
for i, pkg := range active {
|
||||
result[i] = SearchResultRef{Source: pkg.Source, Name: pkg.Name}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *Engine) postInstallTable(event *PostInstallEvent) *glua.LTable {
|
||||
eventTable, data := e.newEventTable(EventPostInstall)
|
||||
data.RawSetString("packages", e.postInstallPackagesTable(event.Packages))
|
||||
|
||||
return eventTable
|
||||
}
|
||||
|
||||
func (e *Engine) postInstallPackagesTable(packages []PostInstallPackage) *glua.LTable {
|
||||
tbl := e.L.NewTable()
|
||||
|
||||
for i := range packages {
|
||||
pkg := &packages[i]
|
||||
pkgTbl := e.L.NewTable()
|
||||
pkgTbl.RawSetString("name", glua.LString(pkg.Name))
|
||||
pkgTbl.RawSetString("version", glua.LString(pkg.Version))
|
||||
pkgTbl.RawSetString("local_version", glua.LString(pkg.LocalVersion))
|
||||
pkgTbl.RawSetString("source", glua.LString(pkg.Source))
|
||||
pkgTbl.RawSetString("reason", glua.LString(pkg.Reason))
|
||||
pkgTbl.RawSetString("installed", glua.LBool(pkg.Installed))
|
||||
pkgTbl.RawSetString("upgrade", glua.LBool(pkg.Upgrade))
|
||||
pkgTbl.RawSetString("devel", glua.LBool(pkg.Devel))
|
||||
tbl.Append(pkgTbl)
|
||||
}
|
||||
|
||||
return tbl
|
||||
}
|
||||
|
||||
func (e *Engine) searchFilterTable(packages []SearchResultPackage) *glua.LTable {
|
||||
eventTable, data := e.newEventTable(EventSearchFilter)
|
||||
data.RawSetString("results", e.searchResultPackagesTable(packages))
|
||||
|
||||
return eventTable
|
||||
}
|
||||
|
||||
func (e *Engine) searchResultPackagesTable(packages []SearchResultPackage) *glua.LTable {
|
||||
tbl := e.L.NewTable()
|
||||
|
||||
for i := range packages {
|
||||
pkg := &packages[i]
|
||||
pkgTbl := e.L.NewTable()
|
||||
pkgTbl.RawSetString("source", glua.LString(pkg.Source))
|
||||
pkgTbl.RawSetString("name", glua.LString(pkg.Name))
|
||||
pkgTbl.RawSetString("description", glua.LString(pkg.Description))
|
||||
pkgTbl.RawSetString("base", glua.LString(pkg.Base))
|
||||
pkgTbl.RawSetString("votes", glua.LNumber(pkg.Votes))
|
||||
pkgTbl.RawSetString("popularity", glua.LNumber(pkg.Popularity))
|
||||
pkgTbl.RawSetString("first_submitted", glua.LNumber(pkg.FirstSubmitted))
|
||||
pkgTbl.RawSetString("last_modified", glua.LNumber(pkg.LastModified))
|
||||
pkgTbl.RawSetString("provides", e.stringArray(pkg.Provides))
|
||||
tbl.Append(pkgTbl)
|
||||
}
|
||||
|
||||
return tbl
|
||||
}
|
||||
|
||||
func parseSearchFilterResult(value glua.LValue, valid map[SearchResultRef]int) ([]SearchResultRef, bool, error) {
|
||||
if value == glua.LNil {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
tbl, ok := value.(*glua.LTable)
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("callback must return nil or a table, got %s", value.Type())
|
||||
}
|
||||
|
||||
var (
|
||||
refs []SearchResultRef
|
||||
parseErr error
|
||||
)
|
||||
|
||||
seen := mapset.NewThreadUnsafeSet[SearchResultRef]()
|
||||
|
||||
tbl.ForEach(func(_ glua.LValue, val glua.LValue) {
|
||||
if parseErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
entry, ok := val.(*glua.LTable)
|
||||
if !ok {
|
||||
parseErr = fmt.Errorf("each result must be a table")
|
||||
return
|
||||
}
|
||||
|
||||
source, ok := entry.RawGetString("source").(glua.LString)
|
||||
if !ok {
|
||||
parseErr = fmt.Errorf("result source must be a string")
|
||||
return
|
||||
}
|
||||
|
||||
name, ok := entry.RawGetString("name").(glua.LString)
|
||||
if !ok {
|
||||
parseErr = fmt.Errorf("result name must be a string")
|
||||
return
|
||||
}
|
||||
|
||||
ref := SearchResultRef{Source: string(source), Name: string(name)}
|
||||
if _, exists := valid[ref]; !exists {
|
||||
parseErr = fmt.Errorf("unknown search result %s/%s", ref.Source, ref.Name)
|
||||
return
|
||||
}
|
||||
|
||||
if !seen.Add(ref) {
|
||||
return
|
||||
}
|
||||
|
||||
refs = append(refs, ref)
|
||||
})
|
||||
|
||||
if parseErr != nil {
|
||||
return nil, false, parseErr
|
||||
}
|
||||
|
||||
return refs, true, nil
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
glua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func TestRunPostInstallEventTableShape(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := New()
|
||||
defer e.Close()
|
||||
|
||||
ran := false
|
||||
e.L.SetGlobal("setRan", e.L.NewFunction(func(_ *glua.LState) int {
|
||||
ran = true
|
||||
return 0
|
||||
}))
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("PostInstall", {
|
||||
callback = function(event)
|
||||
if event.event ~= "PostInstall" then error("bad event name") end
|
||||
if event.data == nil then error("missing data") end
|
||||
local pkg = event.data.packages[1]
|
||||
if pkg == nil then error("missing package") end
|
||||
if pkg.name ~= "mypkg" then error("bad name: " .. tostring(pkg.name)) end
|
||||
if pkg.version ~= "1.2.3-1" then error("bad version") end
|
||||
if pkg.local_version ~= "1.0.0-1" then error("bad local_version") end
|
||||
if pkg.source ~= "aur" then error("bad source") end
|
||||
if pkg.reason ~= "explicit" then error("bad reason") end
|
||||
if pkg.installed ~= true then error("bad installed") end
|
||||
if pkg.upgrade ~= false then error("bad upgrade") end
|
||||
if pkg.devel ~= true then error("bad devel") end
|
||||
setRan()
|
||||
end,
|
||||
})
|
||||
`))
|
||||
|
||||
err := e.RunPostInstall(&PostInstallEvent{
|
||||
Packages: []PostInstallPackage{
|
||||
{
|
||||
Name: "mypkg",
|
||||
Version: "1.2.3-1",
|
||||
LocalVersion: "1.0.0-1",
|
||||
Source: "aur",
|
||||
Reason: "explicit",
|
||||
Installed: true,
|
||||
Upgrade: false,
|
||||
Devel: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, ran)
|
||||
}
|
||||
|
||||
func TestRunPostInstallReturnsAbortWithoutTraceback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("PostInstall", {
|
||||
callback = function()
|
||||
yay.abort("blocked")
|
||||
end,
|
||||
})
|
||||
`))
|
||||
|
||||
err := e.RunPostInstall(&PostInstallEvent{
|
||||
Packages: []PostInstallPackage{{Name: "mypkg"}},
|
||||
})
|
||||
require.EqualError(t, err, "PostInstall: blocked")
|
||||
}
|
||||
|
||||
func TestRunSearchFilterEventTableShapeAndReturn(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("SearchFilter", {
|
||||
callback = function(event)
|
||||
if event.event ~= "SearchFilter" then error("bad event name") end
|
||||
local r = event.data.results[1]
|
||||
if r.source ~= "aur" then error("bad source") end
|
||||
if r.name ~= "pkgA" then error("bad name") end
|
||||
if r.description ~= "desc A" then error("bad description") end
|
||||
if r.base ~= "pkgA" then error("bad base") end
|
||||
if r.votes ~= 42 then error("bad votes") end
|
||||
if math.abs(r.popularity - 3.14) > 0.001 then error("bad popularity") end
|
||||
if r.first_submitted ~= 1000 then error("bad first_submitted") end
|
||||
if r.last_modified ~= 2000 then error("bad last_modified") end
|
||||
if r.provides[1] ~= "pkgA-compat" then error("bad provides") end
|
||||
|
||||
-- Return reversed order, dropping pkgC
|
||||
return {
|
||||
{ source = "sync", name = "pkgB" },
|
||||
{ source = "aur", name = "pkgA" },
|
||||
}
|
||||
end,
|
||||
})
|
||||
`))
|
||||
|
||||
refs, err := e.RunSearchFilter(&SearchFilterEvent{
|
||||
Results: []SearchResultPackage{
|
||||
{
|
||||
Source: "aur",
|
||||
Name: "pkgA",
|
||||
Description: "desc A",
|
||||
Base: "pkgA",
|
||||
Votes: 42,
|
||||
Popularity: 3.14,
|
||||
FirstSubmitted: 1000,
|
||||
LastModified: 2000,
|
||||
Provides: []string{"pkgA-compat"},
|
||||
},
|
||||
{Source: "sync", Name: "pkgB"},
|
||||
{Source: "aur", Name: "pkgC"},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []SearchResultRef{
|
||||
{Source: "sync", Name: "pkgB"},
|
||||
{Source: "aur", Name: "pkgA"},
|
||||
}, refs)
|
||||
}
|
||||
|
||||
func TestRunSearchFilterNilReturnMeansUnchanged(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("SearchFilter", {
|
||||
callback = function(event)
|
||||
-- return nothing
|
||||
end,
|
||||
})
|
||||
`))
|
||||
|
||||
refs, err := e.RunSearchFilter(&SearchFilterEvent{
|
||||
Results: []SearchResultPackage{
|
||||
{Source: "aur", Name: "pkgA"},
|
||||
{Source: "sync", Name: "pkgB"},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []SearchResultRef{
|
||||
{Source: "aur", Name: "pkgA"},
|
||||
{Source: "sync", Name: "pkgB"},
|
||||
}, refs)
|
||||
}
|
||||
|
||||
func TestRunSearchFilterRejectsUnknownResult(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := New()
|
||||
defer e.Close()
|
||||
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("SearchFilter", {
|
||||
callback = function()
|
||||
return { { source = "x", name = "ghost" } }
|
||||
end,
|
||||
})
|
||||
`))
|
||||
|
||||
_, err := e.RunSearchFilter(&SearchFilterEvent{
|
||||
Results: []SearchResultPackage{
|
||||
{Source: "aur", Name: "pkgA"},
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "unknown search result x/ghost")
|
||||
}
|
||||
|
||||
func TestRunSearchFilterChainsMultipleHooks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := New()
|
||||
defer e.Close()
|
||||
|
||||
// First hook drops pkgC.
|
||||
// Second hook sees only pkgA and pkgB, reorders them.
|
||||
require.NoError(t, e.L.DoString(`
|
||||
yay.create_autocmd("SearchFilter", {
|
||||
callback = function(event)
|
||||
-- drop pkgC
|
||||
return {
|
||||
{ source = "aur", name = "pkgA" },
|
||||
{ source = "sync", name = "pkgB" },
|
||||
}
|
||||
end,
|
||||
})
|
||||
yay.create_autocmd("SearchFilter", {
|
||||
callback = function(event)
|
||||
if #event.data.results ~= 2 then error("expected 2 results, got " .. #event.data.results) end
|
||||
-- reorder: B then A
|
||||
return {
|
||||
{ source = "sync", name = "pkgB" },
|
||||
{ source = "aur", name = "pkgA" },
|
||||
}
|
||||
end,
|
||||
})
|
||||
`))
|
||||
|
||||
refs, err := e.RunSearchFilter(&SearchFilterEvent{
|
||||
Results: []SearchResultPackage{
|
||||
{Source: "aur", Name: "pkgA"},
|
||||
{Source: "sync", Name: "pkgB"},
|
||||
{Source: "aur", Name: "pkgC"},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []SearchResultRef{
|
||||
{Source: "sync", Name: "pkgB"},
|
||||
{Source: "aur", Name: "pkgA"},
|
||||
}, refs)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package sync
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/Jguer/yay/v12/pkg/dep"
|
||||
settingslua "github.com/Jguer/yay/v12/pkg/settings/lua"
|
||||
)
|
||||
|
||||
// postInstallEvent flattens the resolved topo layers into the PostInstall
|
||||
// payload. Packages recorded in failedAndIgnored (last-layer AUR build
|
||||
// failures tolerated by the installer) are marked installed = false.
|
||||
func postInstallEvent(targets []map[string]*dep.InstallInfo, failedAndIgnored map[string]error) *settingslua.PostInstallEvent {
|
||||
merged := map[string]*dep.InstallInfo{}
|
||||
for _, layer := range targets {
|
||||
for name, info := range layer {
|
||||
merged[name] = info
|
||||
}
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(merged))
|
||||
for name := range merged {
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
sort.Strings(names)
|
||||
|
||||
packages := make([]settingslua.PostInstallPackage, 0, len(names))
|
||||
for _, name := range names {
|
||||
info := merged[name]
|
||||
_, failed := failedAndIgnored[name]
|
||||
packages = append(packages, settingslua.PostInstallPackage{
|
||||
Name: name,
|
||||
Version: info.Version,
|
||||
LocalVersion: info.LocalVersion,
|
||||
Source: luaSource(info.Source),
|
||||
Reason: luaReason(info.Reason),
|
||||
Installed: !failed,
|
||||
Upgrade: info.Upgrade,
|
||||
Devel: info.Devel,
|
||||
})
|
||||
}
|
||||
|
||||
return &settingslua.PostInstallEvent{Packages: packages}
|
||||
}
|
||||
|
||||
func luaSource(source dep.Source) string {
|
||||
switch source {
|
||||
case dep.AUR:
|
||||
return "aur"
|
||||
case dep.Sync:
|
||||
return "sync"
|
||||
case dep.Local:
|
||||
return "local"
|
||||
case dep.SrcInfo:
|
||||
return "srcinfo"
|
||||
default:
|
||||
return "missing"
|
||||
}
|
||||
}
|
||||
|
||||
func luaReason(reason dep.Reason) string {
|
||||
switch reason {
|
||||
case dep.Explicit:
|
||||
return "explicit"
|
||||
case dep.Dep:
|
||||
return "dependency"
|
||||
case dep.MakeDep:
|
||||
return "make_dependency"
|
||||
case dep.CheckDep:
|
||||
return "check_dependency"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
//go:build !integration
|
||||
// +build !integration
|
||||
|
||||
package sync
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/Jguer/yay/v12/pkg/dep"
|
||||
settingslua "github.com/Jguer/yay/v12/pkg/settings/lua"
|
||||
)
|
||||
|
||||
func TestPostInstallEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
base := "aur-base"
|
||||
targets := []map[string]*dep.InstallInfo{
|
||||
{
|
||||
// Layer 0: two AUR packages, one of which will fail.
|
||||
"pkgA": {
|
||||
Source: dep.AUR,
|
||||
Reason: dep.Explicit,
|
||||
Version: "2.0-1",
|
||||
LocalVersion: "1.0-1",
|
||||
AURBase: &base,
|
||||
Upgrade: true,
|
||||
Devel: false,
|
||||
},
|
||||
"pkgB": {
|
||||
Source: dep.AUR,
|
||||
Reason: dep.Dep,
|
||||
Version: "1.1-1",
|
||||
LocalVersion: "",
|
||||
AURBase: &base,
|
||||
Upgrade: false,
|
||||
Devel: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Layer 1: one Sync package and a duplicate of pkgA (rollup case).
|
||||
"pkgC": {
|
||||
Source: dep.Sync,
|
||||
Reason: dep.MakeDep,
|
||||
Version: "3.0-1",
|
||||
Upgrade: false,
|
||||
Devel: false,
|
||||
},
|
||||
// pkgA appears in layer 1 too; layer merge last-wins → this version.
|
||||
"pkgA": {
|
||||
Source: dep.AUR,
|
||||
Reason: dep.Explicit,
|
||||
Version: "2.0-2",
|
||||
LocalVersion: "1.0-1",
|
||||
AURBase: &base,
|
||||
Upgrade: true,
|
||||
Devel: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// pkgB failed to install.
|
||||
failedAndIgnored := map[string]error{
|
||||
"pkgB": assert.AnError,
|
||||
}
|
||||
|
||||
event := postInstallEvent(targets, failedAndIgnored)
|
||||
|
||||
// Must be sorted by name.
|
||||
want := &settingslua.PostInstallEvent{
|
||||
Packages: []settingslua.PostInstallPackage{
|
||||
{
|
||||
Name: "pkgA",
|
||||
Version: "2.0-2",
|
||||
LocalVersion: "1.0-1",
|
||||
Source: "aur",
|
||||
Reason: "explicit",
|
||||
Installed: true,
|
||||
Upgrade: true,
|
||||
Devel: false,
|
||||
},
|
||||
{
|
||||
Name: "pkgB",
|
||||
Version: "1.1-1",
|
||||
LocalVersion: "",
|
||||
Source: "aur",
|
||||
Reason: "dependency",
|
||||
Installed: false, // in failedAndIgnored
|
||||
Upgrade: false,
|
||||
Devel: true,
|
||||
},
|
||||
{
|
||||
Name: "pkgC",
|
||||
Version: "3.0-1",
|
||||
Source: "sync",
|
||||
Reason: "make_dependency",
|
||||
Installed: true,
|
||||
Upgrade: false,
|
||||
Devel: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, want, event)
|
||||
}
|
||||
|
||||
func TestPostInstallEventSourceAndReasonMapping(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
source dep.Source
|
||||
reason dep.Reason
|
||||
wantSource string
|
||||
wantReason string
|
||||
}{
|
||||
{"aur explicit", dep.AUR, dep.Explicit, "aur", "explicit"},
|
||||
{"sync dep", dep.Sync, dep.Dep, "sync", "dependency"},
|
||||
{"local makedep", dep.Local, dep.MakeDep, "local", "make_dependency"},
|
||||
{"srcinfo checkdep", dep.SrcInfo, dep.CheckDep, "srcinfo", "check_dependency"},
|
||||
{"missing unknown", dep.Missing, dep.Reason(99), "missing", "unknown"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, tt.wantSource, luaSource(tt.source))
|
||||
assert.Equal(t, tt.wantReason, luaReason(tt.reason))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/Jguer/yay/v12/pkg/multierror"
|
||||
"github.com/Jguer/yay/v12/pkg/runtime"
|
||||
"github.com/Jguer/yay/v12/pkg/settings"
|
||||
settingslua "github.com/Jguer/yay/v12/pkg/settings/lua"
|
||||
"github.com/Jguer/yay/v12/pkg/settings/parser"
|
||||
"github.com/Jguer/yay/v12/pkg/sync/build"
|
||||
"github.com/Jguer/yay/v12/pkg/sync/srcinfo"
|
||||
@@ -127,6 +128,13 @@ func (o *OperationService) Run(ctx context.Context, run *runtime.Runtime,
|
||||
multiErr.Add(err)
|
||||
}
|
||||
|
||||
if !cmdArgs.ExistsArg("w", "downloadonly") && run.Lua != nil &&
|
||||
run.Lua.HasAutocmd(settingslua.EventPostInstall) {
|
||||
if err := run.Lua.RunPostInstall(postInstallEvent(targets, failedAndIgnored)); err != nil {
|
||||
multiErr.Add(err)
|
||||
}
|
||||
}
|
||||
|
||||
return multiErr.Return()
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user