fix: propagate Maintainer field through dep graph and upgrade service (#2865)

* fix: propagate Maintainer field through dep graph and upgrade service

Add Maintainer to InstallInfo and propagate it through the dependency
graph (dep_resolve, dep_visual) and UpgradeService. Update docs
generator with TOC support and Lua page builder.

* lint
This commit is contained in:
Jo authored and GitHub committed 2026-06-16 12:19:19 +02:00
1 parent a70c058875
commit 6927555779
9 files changed
+181 -24

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
-- Warn when an AUR package's maintainer changes between upgrades.
--
-- The known maintainer for each package is stored in a plain text cache file
-- inside the yay cache directory (build_dir). On the first upgrade for a
-- package the current maintainer is recorded without any warning. On
-- subsequent upgrades:
-- * same maintainer → debug "match correct"
-- * different maintainer → error "new maintainer, double check build files"
--
-- The cache is updated whenever a new or changed maintainer is seen.
--
-- Cache file location: <build_dir>/maintainer_cache
-- Format: one "pkgname=maintainer" entry per line.
local cache_file = yay.opt.build_dir .. "/maintainer_cache"
local function load_cache()
local cache = {}
local f = io.open(cache_file, "r")
if not f then return cache end
for line in f:lines() do
local name, maintainer = line:match("^([^=]+)=(.*)$")
if name then
cache[name] = maintainer
end
end
f:close()
return cache
end
local function save_cache(cache)
local f = assert(io.open(cache_file, "w"))
for name, maintainer in pairs(cache) do
f:write(name .. "=" .. maintainer .. "\n")
end
f:close()
end
yay.create_autocmd("UpgradeSelect", {
desc = "warn on AUR maintainer changes",
callback = function(event)
yay.log.info("checking for AUR maintainer changes")
local cache = load_cache()
local dirty = false
for _, pkg in ipairs(event.data.upgrades) do
if pkg.repository == "aur" and pkg.maintainer ~= "" then
local cached = cache[pkg.name]
if cached == nil then
-- First time seeing this package: seed the cache silently.
cache[pkg.name] = pkg.maintainer
dirty = true
elseif cached == pkg.maintainer then
yay.log.debug("match correct: " .. pkg.name .. " " .. pkg.maintainer)
else
yay.log.error("new maintainer, double check build files: ", pkg.name,
"(was: " .. cached .. ", now: " .. pkg.maintainer .. ")")
cache[pkg.name] = pkg.maintainer
dirty = true
end
end
end
if dirty then
yay.log.info("saving maintainer cache:", cache_file)
save_cache(cache)
end
return { exclude = {}, skip_menu = false }
end,
})
-4
View File
@@ -52,10 +52,6 @@ yay.opt.debug = false -- Enable debug logging and local init.lua lookup convenie
yay.opt.rpc = true -- Use AUR RPC for dependency/query operations.
yay.opt.double_confirm = true -- Ask for confirmation before and after builds during upgrades.
-- Logging
-- yay.log.info("loaded yay init.lua")
-- yay.log.debug("build dir:", yay.opt.build_dir)
-- Hooks
-- Run Lua before yay prints the upgrade exclusion menu. Return package names
-- from event.data.upgrades to pre-exclude them. Set skip_menu = false, or omit
+10
View File
@@ -11,6 +11,8 @@ yay can optionally load a Lua configuration file, `init.lua`. `init.lua` overlay
## Setting options with `yay.opt`
<p class="api-since">Available from yay v13.0.0</p>
Assign to keys on the `yay.opt` table using the exact option names shown
below.
@@ -50,6 +52,8 @@ lives at [`doc/init.lua`](init.lua).
## Logging with `yay.log`
<p class="api-since">Available from yay v13.0.0</p>
Lua config and hooks can write through yay's normal logger:
```lua
@@ -65,6 +69,8 @@ hook stops.
## Upgrade selection hooks
<p class="api-since">Available from yay v13.0.0</p>
`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
@@ -143,6 +149,8 @@ native menu. `pulled_dependencies` entries are shown separately by yay and use
## AUR pre-install hooks
<p class="api-since">Available from yay v13.0.0</p>
`init.lua` can register hooks with a small autocmd API:
```lua
@@ -249,6 +257,8 @@ yay.create_autocmd("AURPreInstall", {
## AUR post-download hooks
<p class="api-since">Available from yay v13.0.0</p>
`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.
+4 -1
View File
@@ -31,7 +31,8 @@ type InstallInfo struct {
IsGroup bool
Upgrade bool
Devel bool
LastModified int64 // Unix timestamp, non-zero only for AUR packages
LastModified int64 // Unix timestamp, non-zero only for AUR packages
Maintainer string // AUR maintainer username, empty for orphaned or non-AUR packages
}
func (i *InstallInfo) String() string {
@@ -470,6 +471,7 @@ func (g *Grapher) GraphFromAUR(ctx context.Context,
Source: AUR,
Version: aurPkg.Version,
LastModified: int64(aurPkg.LastModified),
Maintainer: aurPkg.Maintainer,
})
aurPkgsAdded = append(aurPkgsAdded, aurPkg)
}
@@ -727,6 +729,7 @@ func (g *Grapher) addNodes(
AURBase: &aurPkg.PackageBase,
Version: aurPkg.Version,
LastModified: int64(aurPkg.LastModified),
Maintainer: aurPkg.Maintainer,
},
})
+10 -10
View File
@@ -283,11 +283,11 @@ func TestGrapher_SplitPackages_Clion(t *testing.T) {
}}
installInfos := map[string]*InstallInfo{
"clion exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120},
"clion-jre exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120},
"clion-cmake exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120},
"clion-gdb exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120},
"clion-lldb exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120},
"clion exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120, Maintainer: "Zrax"},
"clion-jre exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120, Maintainer: "Zrax"},
"clion-cmake exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120, Maintainer: "Zrax"},
"clion-gdb exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120, Maintainer: "Zrax"},
"clion-lldb exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120, Maintainer: "Zrax"},
}
tests := []struct {
@@ -565,11 +565,11 @@ func TestGrapher_SplitPackages_NX(t *testing.T) {
}}
installInfos := map[string]*InstallInfo{
"nxproxy exp": {Source: AUR, Reason: Explicit, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449},
"nxagent exp": {Source: AUR, Reason: Explicit, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449},
"nx-x11 dep": {Source: AUR, Reason: Dep, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449},
"libxcomp dep": {Source: AUR, Reason: Dep, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449},
"libxcomp exp": {Source: AUR, Reason: Explicit, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449},
"nxproxy exp": {Source: AUR, Reason: Explicit, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449, Maintainer: "harrietobrien"},
"nxagent exp": {Source: AUR, Reason: Explicit, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449, Maintainer: "harrietobrien"},
"nx-x11 dep": {Source: AUR, Reason: Dep, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449, Maintainer: "harrietobrien"},
"libxcomp dep": {Source: AUR, Reason: Dep, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449, Maintainer: "harrietobrien"},
"libxcomp exp": {Source: AUR, Reason: Explicit, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449, Maintainer: "harrietobrien"},
}
tests := []struct {
+10
View File
@@ -167,6 +167,7 @@ func TestGrapher_GraphFromTargets_jellyfin(t *testing.T) {
Version: "10.8.8-1",
AURBase: ptrString("jellyfin"),
LastModified: 1669830147,
Maintainer: "z3ntu",
},
},
{
@@ -199,6 +200,7 @@ func TestGrapher_GraphFromTargets_jellyfin(t *testing.T) {
Version: "10.8.8-1",
AURBase: ptrString("jellyfin"),
LastModified: 1669830147,
Maintainer: "z3ntu",
},
},
{
@@ -208,6 +210,7 @@ func TestGrapher_GraphFromTargets_jellyfin(t *testing.T) {
Version: "10.8.8-1",
AURBase: ptrString("jellyfin"),
LastModified: 1669830147,
Maintainer: "z3ntu",
},
"jellyfin-server": {
Source: AUR,
@@ -215,6 +218,7 @@ func TestGrapher_GraphFromTargets_jellyfin(t *testing.T) {
Version: "10.8.8-1",
AURBase: ptrString("jellyfin"),
LastModified: 1669830147,
Maintainer: "z3ntu",
},
},
{
@@ -337,6 +341,7 @@ func TestGrapher_GraphProvides_androidsdk(t *testing.T) {
Version: "26.1.1-2",
AURBase: ptrString("android-sdk"),
LastModified: 1647982720,
Maintainer: "dreamingincode",
},
},
{
@@ -1022,6 +1027,7 @@ func TestGrapher_GraphFromAUR_SplitPkgInternalDeps(t *testing.T) {
Version: "1.24.0.r37-1",
AURBase: ptrString("gstreamer-git"),
LastModified: 1700000000,
Maintainer: "testmaint",
},
"gstreamer-git dep": {
Source: AUR,
@@ -1029,6 +1035,7 @@ func TestGrapher_GraphFromAUR_SplitPkgInternalDeps(t *testing.T) {
Version: "1.24.0.r37-1",
AURBase: ptrString("gstreamer-git"),
LastModified: 1700000000,
Maintainer: "testmaint",
},
"gst-plugins-base-libs-git exp": {
Source: AUR,
@@ -1036,6 +1043,7 @@ func TestGrapher_GraphFromAUR_SplitPkgInternalDeps(t *testing.T) {
Version: "1.24.0.r37-1",
AURBase: ptrString("gstreamer-git"),
LastModified: 1700000000,
Maintainer: "testmaint",
},
"gst-plugins-base-libs-git dep": {
Source: AUR,
@@ -1043,6 +1051,7 @@ func TestGrapher_GraphFromAUR_SplitPkgInternalDeps(t *testing.T) {
Version: "1.24.0.r37-1",
AURBase: ptrString("gstreamer-git"),
LastModified: 1700000000,
Maintainer: "testmaint",
},
"gst-plugins-good-git exp": {
Source: AUR,
@@ -1050,6 +1059,7 @@ func TestGrapher_GraphFromAUR_SplitPkgInternalDeps(t *testing.T) {
Version: "1.24.0.r37-1",
AURBase: ptrString("gstreamer-git"),
LastModified: 1700000000,
Maintainer: "testmaint",
},
}
+3
View File
@@ -126,6 +126,7 @@ func (u *UpgradeService) upGraph(ctx context.Context, graph *topo.Graph[string,
LocalVersion: up.LocalVersion,
Version: up.RemoteVersion,
LastModified: up.LastModified,
Maintainer: aurPkg.Maintainer,
})
names.Add(up.Name)
aurPkgsAdded = append(aurPkgsAdded, aurPkg)
@@ -157,6 +158,7 @@ func (u *UpgradeService) upGraph(ctx context.Context, graph *topo.Graph[string,
Version: up.RemoteVersion,
LocalVersion: up.LocalVersion,
LastModified: up.LastModified,
Maintainer: aurPkg.Maintainer,
})
aurPkgsAdded = append(aurPkgsAdded, aurPkg)
}
@@ -224,6 +226,7 @@ func (u *UpgradeService) graphToUpSlice(graph *topo.Graph[string, *dep.InstallIn
Reason: alpmReason,
Extra: extra,
LastModified: info.LastModified,
Maintainer: info.Maintainer,
})
case dep.Sync:
repoUp.Up = append(repoUp.Up, Upgrade{
+5 -1
View File
@@ -125,6 +125,7 @@ func TestUpgradeService_GraphUpgrades(t *testing.T) {
Version: "latest-commit",
Upgrade: true,
Devel: true,
Maintainer: "morganamilo",
}
newDepInfo := &dep.InstallInfo{
@@ -145,6 +146,7 @@ func TestUpgradeService_GraphUpgrades(t *testing.T) {
Version: "2.2.1.r69.g8a10460-1",
Upgrade: true,
Devel: false,
Maintainer: "morganamilo",
}
yayDepInfo := &dep.InstallInfo{
@@ -155,6 +157,7 @@ func TestUpgradeService_GraphUpgrades(t *testing.T) {
Version: "10.2.4",
Upgrade: true,
Devel: false,
Maintainer: "jguer",
}
coreDB := mock.NewDB("core")
@@ -227,10 +230,11 @@ func TestUpgradeService_GraphUpgrades(t *testing.T) {
mockAUR := &mockaur.MockAUR{
GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
return []aur.Pkg{
{Name: "yay", Version: "10.2.4", PackageBase: "yay"},
{Name: "yay", Version: "10.2.4", PackageBase: "yay", Maintainer: "jguer"},
{
Name: "example-git", Version: "2.2.1.r69.g8a10460-1",
PackageBase: "example", Depends: []string{"new-dep"},
Maintainer: "morganamilo",
},
}, nil
},
+68 -8
View File
@@ -21,6 +21,7 @@ import (
"html/template"
"os"
"path/filepath"
"regexp"
"strings"
bf "github.com/russross/blackfriday/v2"
@@ -37,20 +38,21 @@ func main() {
// yay.8 (troff man page) → Markdown → HTML
man8 := mustRead(filepath.Join(*docs, "yay.8"))
writePage(*out, "man.html", "yay(8) Manual", mdToHTML(troff2md(man8)))
writePage(*out, "man.html", pageData{Title: "yay(8) Manual", Body: template.HTML(mdToHTML(troff2md(man8)))})
// lua.md → HTML
// lua.md → HTML with TOC and version badges
luaMD := mustRead(filepath.Join(*docs, "lua.md"))
writePage(*out, "lua.html", "Lua API", mdToHTML(luaMD))
luaBody, luaTOC := buildLuaPage(mdToHTML(luaMD))
writePage(*out, "lua.html", pageData{Title: "Lua API", Body: luaBody, TOC: luaTOC})
// init.lua source as a code block
initLua := mustRead(filepath.Join(*docs, "init.lua"))
initMD := "# init.lua template\n\n```lua\n" + string(initLua) + "```\n"
writePage(*out, "init-lua.html", "init.lua template", mdToHTML([]byte(initMD)))
writePage(*out, "init-lua.html", pageData{Title: "init.lua template", Body: template.HTML(mdToHTML([]byte(initMD)))})
// index / landing page
indexMD := mustRead(filepath.Join(*docs, "index.md"))
writePage(*out, "index.html", "yay", mdToHTML(indexMD))
writePage(*out, "index.html", pageData{Title: "yay", Body: template.HTML(mdToHTML(indexMD))})
fmt.Printf("site written to %s\n", *out)
}
@@ -81,6 +83,15 @@ pre code{background:none;padding:0;font-size:.85em}
a{color:#0067c0}
strong{font-weight:600}
footer{margin-top:3rem;padding-top:1rem;border-top:1px solid #e0e0e0;color:#666;font-size:.875rem}
.page-toc{position:fixed;top:5rem;right:1rem;width:220px;max-height:calc(100vh - 6rem);overflow-y:auto;background:#f9f9f9;border:1px solid #e0e0e0;border-radius:4px;padding:.75rem 1rem}
.page-toc strong{display:block;margin-bottom:.5rem;color:#555;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em}
.page-toc ul{list-style:none;margin:0;padding:0}
.page-toc li{margin:.15rem 0}
.page-toc a{color:#0067c0;text-decoration:none;font-size:.8rem;line-height:1.4;display:block}
.page-toc a:hover{text-decoration:underline}
.toc-h3{padding-left:.75rem}
@media(max-width:1200px){.page-toc{display:none}}
.api-since{font-size:.8rem;color:#666;font-style:italic;margin:-.25rem 0 .75rem}
</style>
</head>
<body>
@@ -90,6 +101,7 @@ footer{margin-top:3rem;padding-top:1rem;border-top:1px solid #e0e0e0;color:#666;
<a href="lua.html">Lua API</a>
<a href="init-lua.html">init.lua</a>
</nav>
{{if .TOC}}<aside class="page-toc">{{.TOC}}</aside>{{end}}
<main>
{{.Body}}
</main>
@@ -100,21 +112,70 @@ footer{margin-top:3rem;padding-top:1rem;border-top:1px solid #e0e0e0;color:#666;
type pageData struct {
Title string
Body template.HTML
TOC template.HTML // optional; non-empty → TOC sidebar is rendered
}
func writePage(dir, name, title string, body []byte) {
func writePage(dir, name string, data pageData) {
path := filepath.Join(dir, name)
f, err := os.Create(path)
if err != nil {
fatal(err)
}
defer f.Close()
if err := pageTmpl.Execute(f, pageData{Title: title, Body: template.HTML(body)}); err != nil {
if err := pageTmpl.Execute(f, data); err != nil {
fatal(err)
}
fmt.Printf(" %s\n", path)
}
// ── Lua page: TOC + version badges ────────────────────────────────────────────
// reHeading matches an h2 or h3 element with an id attribute as produced by
// blackfriday's AutoHeadingIDs extension. The (?s) flag makes . match \n so
// multi-line heading content (rare but possible) is covered.
var reHeading = regexp.MustCompile(`(?s)<(h[23]) id="([^"]+)"[^>]*>(.*?)</h[23]>`)
// reTag strips HTML tags to obtain plain text for TOC labels.
var reTag = regexp.MustCompile(`<[^>]+>`)
// buildLuaPage extracts headings for the table of contents and returns the
// body HTML unchanged alongside the TOC HTML.
func buildLuaPage(body []byte) (newBody template.HTML, toc template.HTML) {
s := string(body)
// Collect headings in document order to build the TOC.
type entry struct {
level int
id string
label string // plain text, HTML-escaped when written into TOC
}
var entries []entry
for _, m := range reHeading.FindAllStringSubmatch(s, -1) {
lvl := 2
if m[1] == "h3" {
lvl = 3
}
// Strip inner tags (e.g. <code>) to get a plain-text label.
label := strings.TrimSpace(reTag.ReplaceAllString(m[3], ""))
entries = append(entries, entry{lvl, m[2], label})
}
// Build the TOC HTML: h3 entries are indented via the .toc-h3 class.
var b strings.Builder
b.WriteString("<strong>On this page</strong>\n<ul>\n")
for _, e := range entries {
class := ""
if e.level == 3 {
class = ` class="toc-h3"`
}
fmt.Fprintf(&b, "<li%s><a href=\"#%s\">%s</a></li>\n",
class, e.id, template.HTMLEscapeString(e.label))
}
b.WriteString("</ul>")
return template.HTML(s), template.HTML(b.String())
}
// ── Markdown → HTML ───────────────────────────────────────────────────────────
func mdToHTML(src []byte) []byte {
@@ -366,4 +427,3 @@ func fatal(err error) {
fmt.Fprintln(os.Stderr, "gendocs:", err)
os.Exit(1)
}