pacman_packages answers one package on every node, or each node's counts, its explicit packages no module declares and its foreign ones apart, the versions that differ and the packages on some nodes only, with a node over the bus's cap marked not current. pacman_updates, pacman_installed and pacman_foreign read the keeper's states instead of the machine; pacman_updates says when each node's list last changed.
315 lines
9.1 KiB
Go
315 lines
9.1 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"
|
|
"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"`
|
|
Repository string `json:"repository,omitempty"`
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|