diff --git a/doc/examples/maintainer_change.lua b/doc/examples/maintainer_change.lua new file mode 100644 index 00000000..84410a71 --- /dev/null +++ b/doc/examples/maintainer_change.lua @@ -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: /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, +}) diff --git a/doc/init.lua b/doc/init.lua index efc23359..e0a25680 100644 --- a/doc/init.lua +++ b/doc/init.lua @@ -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 diff --git a/doc/lua.md b/doc/lua.md index e0d14270..cc7c2cd5 100644 --- a/doc/lua.md +++ b/doc/lua.md @@ -11,6 +11,8 @@ yay can optionally load a Lua configuration file, `init.lua`. `init.lua` overlay ## 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. @@ -50,6 +52,8 @@ lives at [`doc/init.lua`](init.lua). ## Logging with `yay.log` +

Available from yay v13.0.0

+ Lua config and hooks can write through yay's normal logger: ```lua @@ -65,6 +69,8 @@ hook stops. ## 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 @@ -143,6 +149,8 @@ native menu. `pulled_dependencies` entries are shown separately by yay and use ## AUR pre-install hooks +

Available from yay v13.0.0

+ `init.lua` can register hooks with a small autocmd API: ```lua @@ -249,6 +257,8 @@ yay.create_autocmd("AURPreInstall", { ## 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. diff --git a/pkg/dep/dep_graph.go b/pkg/dep/dep_graph.go index a3079a9b..b7e4ebb1 100644 --- a/pkg/dep/dep_graph.go +++ b/pkg/dep/dep_graph.go @@ -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, }, }) diff --git a/pkg/dep/dep_graph_rpc_test.go b/pkg/dep/dep_graph_rpc_test.go index d05c67b1..e87b114f 100644 --- a/pkg/dep/dep_graph_rpc_test.go +++ b/pkg/dep/dep_graph_rpc_test.go @@ -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 { diff --git a/pkg/dep/dep_graph_test.go b/pkg/dep/dep_graph_test.go index 44728360..de003dff 100644 --- a/pkg/dep/dep_graph_test.go +++ b/pkg/dep/dep_graph_test.go @@ -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", }, } diff --git a/pkg/upgrade/service.go b/pkg/upgrade/service.go index 61b9391a..bb11c3f8 100644 --- a/pkg/upgrade/service.go +++ b/pkg/upgrade/service.go @@ -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{ diff --git a/pkg/upgrade/service_test.go b/pkg/upgrade/service_test.go index c29ff3ab..b75fe002 100644 --- a/pkg/upgrade/service_test.go +++ b/pkg/upgrade/service_test.go @@ -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 }, diff --git a/scripts/gendocs/main.go b/scripts/gendocs/main.go index 4347df39..5d8acc57 100644 --- a/scripts/gendocs/main.go +++ b/scripts/gendocs/main.go @@ -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} @@ -90,6 +101,7 @@ footer{margin-top:3rem;padding-top:1rem;border-top:1px solid #e0e0e0;color:#666; Lua API init.lua +{{if .TOC}}{{end}}
{{.Body}}
@@ -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="([^"]+)"[^>]*>(.*?)`) + +// 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. ) 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("On this page\n") + + 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) } -