Files
mesh-catalog/modules/pacman/cmd/pacman-tools/query.go
T
jschoubben 1de9a903c8 issue 1840/S6: keep the node's packages in installed, read at the keeper's start and at every released lock
The keeper reads pacman's database into installed.<node> and writes it only when it differs from the key,
says each change as package.changed, and keeps the last value when the list is over the bus's cap, marking
the node not current until it fits again; the health names the size. The loop runs over injected states,
events, clock, watch and machine, and leaves hooks (start, after a transaction, state watches, wakes) for the
upgrade parts of ADR 0508.
2026-10-22 14:31:33 +02:00

395 lines
11 KiB
Go

package main
// Reading the package manager (novox/hq to-be 42 Phase 1, research 026/05): what is installed and
// why, what a search finds, what owns a path, what a package holds, what is orphaned or foreign.
// Every one of these reads the local or sync databases, which any account may; none escalates.
import (
"fmt"
"path"
"regexp"
"sort"
"strings"
)
// Found is one package a search found.
type Found struct {
Repository string `json:"repository"`
Name string `json:"name"`
Version string `json:"version"`
Groups []string `json:"groups,omitempty"`
Installed bool `json:"installed"`
InstalledAs string `json:"installed_version,omitempty"`
Description string `json:"description"`
}
var searchHeader = regexp.MustCompile(`^(\S+)/(\S+) (\S+)(?: \(([^)]*)\))?(?: \[installed(?:: ([^\]]+))?\])?$`)
// ParseSearch reads `pacman -Ss`: a header line per package and its description indented beneath.
func ParseSearch(out string) []Found {
found := []Found{}
for _, l := range strings.Split(out, "\n") {
if strings.TrimSpace(l) == "" {
continue
}
if strings.HasPrefix(l, " ") {
if n := len(found); n > 0 {
found[n-1].Description = strings.TrimSpace(strings.TrimSpace(found[n-1].Description + " " + strings.TrimSpace(l)))
}
continue
}
m := searchHeader.FindStringSubmatch(l)
if m == nil {
continue
}
f := Found{Repository: m[1], Name: m[2], Version: m[3], Installed: strings.Contains(l, "[installed")}
if m[4] != "" {
f.Groups = strings.Fields(m[4])
}
if f.Installed {
f.InstalledAs = f.Version
if m[5] != "" {
f.InstalledAs = m[5]
}
}
found = append(found, f)
}
return found
}
// none is pacman's way of saying a query found nothing: status 1 and nothing said.
func none(r Ran) bool {
return r.Status == 1 && r.Err == "" && strings.TrimSpace(r.Stdout+r.Stderr) == ""
}
// query runs a pacman query whose empty answer is status 1, and fails only on a real failure.
func (m *Machine) query(args ...string) (string, error) {
r := m.Run(bg(), "pacman", args...)
if r.Status == 0 && r.Err == "" {
return r.Stdout, nil
}
if none(r) {
return "", nil
}
return "", failure("pacman", "pacman", r)
}
// Search is `pacman -Ss` over the sync databases, bounded.
func (m *Machine) Search(words string, limit int) (map[string]any, error) {
out, err := m.query(append([]string{"-Ss", "--"}, strings.Fields(words)...)...)
if err != nil {
return nil, err
}
found := ParseSearch(out)
return bound("packages", found, limit), nil
}
// bound is a list answered with its count, cut to a limit and saying so.
func bound[T any](key string, list []T, limit int) map[string]any {
out := map[string]any{"count": len(list), "truncated": false}
if limit > 0 && len(list) > limit {
list = list[:limit]
out["truncated"] = true
}
out[key] = list
return out
}
// ParseInfo reads `pacman -Qi`/`-Si`: `Key : value` lines, continuation lines indented beneath.
// A field that is a list (two spaces between members) is answered as one, and "None" as empty.
func ParseInfo(out string) []map[string]any {
var pkgs []map[string]any
var cur map[string]any
last := ""
for _, l := range strings.Split(out, "\n") {
if strings.TrimSpace(l) == "" {
if cur != nil {
pkgs = append(pkgs, cur)
cur = nil
}
continue
}
if cur == nil {
cur = map[string]any{}
}
if k, v, ok := strings.Cut(l, " : "); ok && !strings.HasPrefix(l, " ") {
last = strings.TrimSpace(k)
cur[last] = infoValue(last, strings.TrimSpace(v))
continue
}
// A continuation: the optional dependencies, one per line.
if last != "" {
v := strings.TrimSpace(l)
switch prev := cur[last].(type) {
case []string:
cur[last] = append(prev, v)
case string:
cur[last] = []string{prev, v}
}
}
}
if cur != nil {
pkgs = append(pkgs, cur)
}
return pkgs
}
var listFields = map[string]bool{
"Licenses": true, "Groups": true, "Provides": true, "Depends On": true, "Optional Deps": true,
"Required By": true, "Optional For": true, "Conflicts With": true, "Replaces": true,
}
func infoValue(key, v string) any {
if !listFields[key] {
return v
}
if v == "None" {
return []string{}
}
if key == "Optional Deps" {
return []string{v}
}
return strings.Fields(v)
}
// Info is one package as the local database knows it, or the sync databases when it is not installed.
func (m *Machine) Info(name string) (map[string]any, error) {
r := m.Run(bg(), "pacman", "-Qi", "--", name)
installed := true
if r.Status != 0 {
if r.Err != "" || !strings.Contains(r.Stderr, "was not found") {
return nil, failure("pacman", "pacman", r)
}
installed = false
if r = m.Run(bg(), "pacman", "-Si", "--", name); r.Status != 0 || r.Err != "" {
if strings.Contains(r.Stderr, "was not found") {
return nil, fmt.Errorf("no package %s, installed or in a repository", name)
}
return nil, failure("pacman", "pacman", r)
}
}
pkgs := ParseInfo(r.Stdout)
if len(pkgs) == 0 {
return nil, fmt.Errorf("pacman said nothing about %s", name)
}
return map[string]any{"installed": installed, "package": pkgs[0]}, nil
}
// Package is an installed package and why it is installed.
type Package struct {
Name string `json:"name"`
Version string `json:"version"`
Reason string `json:"reason"`
Foreign bool `json:"foreign"`
}
func nameVersions(out string) [][2]string {
var nv [][2]string
for _, l := range lines(out) {
if f := strings.Fields(l); len(f) >= 2 {
nv = append(nv, [2]string{f[0], f[1]})
}
}
return nv
}
func nameSet(out string) map[string]bool {
s := map[string]bool{}
for _, l := range lines(out) {
s[strings.TrimSpace(l)] = true
}
return s
}
// Installed is every installed package with its version, whether it was installed explicitly or as
// a dependency, and whether it is foreign (in no repository this machine syncs).
func (m *Machine) Installed(match, reason string, foreignOnly bool, limit int) (map[string]any, error) {
all, err := m.query("-Q")
if err != nil {
return nil, err
}
explicit, err := m.query("-Qeq")
if err != nil {
return nil, err
}
foreign, err := m.query("-Qmq")
if err != nil {
return nil, err
}
ex, fo := nameSet(explicit), nameSet(foreign)
pkgs := []Package{}
counts := map[string]int{"explicit": 0, "dependency": 0, "foreign": 0}
for _, nv := range nameVersions(all) {
p := Package{Name: nv[0], Version: nv[1], Reason: "dependency", Foreign: fo[nv[0]]}
if ex[p.Name] {
p.Reason = "explicit"
}
counts[p.Reason]++
if p.Foreign {
counts["foreign"]++
}
if match != "" && !strings.Contains(p.Name, match) || reason != "" && p.Reason != reason || foreignOnly && !p.Foreign {
continue
}
pkgs = append(pkgs, p)
}
out := bound("packages", pkgs, limit)
out["totals"] = counts
return out, nil
}
var ownedBy = regexp.MustCompile(`^(.*) is owned by (\S+) (\S+)$`)
// Owns is which package owns a path.
func (m *Machine) Owns(p string) (map[string]any, error) {
if !path.IsAbs(p) {
return nil, fmt.Errorf("%q is not an absolute path", p)
}
r := m.Run(bg(), "pacman", "-Qo", "--", p)
if r.Status == 0 && r.Err == "" {
for _, l := range lines(r.Stdout) {
if o := ownedBy.FindStringSubmatch(l); o != nil {
return map[string]any{"path": o[1], "owned": true, "package": o[2], "version": o[3]}, nil
}
}
}
if r.Err == "" && strings.Contains(r.Stderr, "No package owns") {
return map[string]any{"path": p, "owned": false}, nil
}
return nil, failure("pacman", "pacman", r)
}
// Files is what an installed package placed, bounded.
func (m *Machine) Files(name string, limit int) (map[string]any, error) {
r := m.Run(bg(), "pacman", "-Ql", "--", name)
if r.Status != 0 || r.Err != "" {
if strings.Contains(r.Stderr, "was not found") {
return nil, fmt.Errorf("%s is not installed", name)
}
return nil, failure("pacman", "pacman", r)
}
paths := []string{}
for _, l := range lines(r.Stdout) {
if _, p, ok := strings.Cut(l, " "); ok {
paths = append(paths, p)
}
}
out := bound("paths", paths, limit)
out["package"] = name
return out, nil
}
// Orphans are packages installed as dependencies that nothing requires any more.
func (m *Machine) Orphans() (map[string]any, error) {
out, err := m.query("-Qdt")
if err != nil {
return nil, err
}
pkgs := []map[string]string{}
for _, nv := range nameVersions(out) {
pkgs = append(pkgs, map[string]string{"name": nv[0], "version": nv[1]})
}
return map[string]any{"count": len(pkgs), "orphans": pkgs}, nil
}
// Foreign is every installed package no repository this machine syncs carries: built from the AUR
// or by hand, which the host's `package` shape cannot install (research 027, question 1).
func (m *Machine) Foreign() (map[string]any, error) {
out, err := m.query("-Qm")
if err != nil {
return nil, err
}
pkgs := []map[string]string{}
for _, nv := range nameVersions(out) {
pkgs = append(pkgs, map[string]string{"name": nv[0], "version": nv[1]})
}
sort.Slice(pkgs, func(i, j int) bool { return pkgs[i]["name"] < pkgs[j]["name"] })
return map[string]any{"count": len(pkgs), "packages": pkgs}, nil
}
// Update is one package an upgrade would change.
type Update struct {
Name string `json:"name"`
From string `json:"from"`
To string `json:"to"`
}
// Updates is what a full upgrade would change, from checkupdates (UpdateList).
func (m *Machine) Updates() (map[string]any, error) {
list, err := m.UpdateList()
if err != nil {
return nil, err
}
ups := []Update{}
for _, u := range list {
ups = append(ups, Update{u.Name, u.From, u.To})
}
return map[string]any{"count": len(ups), "updates": ups}, nil
}
// Config is the configuration pacman runs with, as pacman-conf resolves it.
type Config struct {
Options map[string][]string `json:"options"`
Repositories []Repository `json:"repositories"`
MeshOwned bool `json:"mesh_owned"`
}
// Repository is one repository and where it is fetched from.
type Repository struct {
Name string `json:"name"`
Servers int `json:"servers"`
FirstServer string `json:"first_server,omitempty"`
SigLevel string `json:"sig_level,omitempty"`
}
// MeshHeader is how the module's pacman.conf begins, which is how it is recognised.
const MeshHeader = "# The mesh's (module pacman"
// ParseConf reads `pacman-conf`: [options] and each repository, with their values.
func ParseConf(out string) Config {
c := Config{Options: map[string][]string{}, Repositories: []Repository{}}
section := ""
for _, l := range lines(out) {
l = strings.TrimSpace(l)
if strings.HasPrefix(l, "[") && strings.HasSuffix(l, "]") {
section = strings.Trim(l, "[]")
if section != "options" {
c.Repositories = append(c.Repositories, Repository{Name: section})
}
continue
}
k, v, _ := strings.Cut(l, " = ")
k, v = strings.TrimSpace(k), strings.TrimSpace(v)
if section == "options" {
c.Options[k] = append(c.Options[k], v)
continue
}
if n := len(c.Repositories); n > 0 {
r := &c.Repositories[n-1]
switch k {
case "Server":
if r.Servers == 0 {
r.FirstServer = v
}
r.Servers++
case "SigLevel":
r.SigLevel = strings.TrimSpace(r.SigLevel + " " + v)
}
}
}
return c
}
// Conf is pacman's configuration in force, and whether /etc/pacman.conf is the module's.
func (m *Machine) Conf() (Config, error) {
out, err := m.Out("pacman-conf")
if err != nil {
return Config{}, err
}
c := ParseConf(out)
if text, err := m.ReadFile("/etc/pacman.conf"); err == nil {
c.MeshOwned = strings.HasPrefix(string(text), MeshHeader)
}
return c, nil
}