Files
mesh-catalog/modules/pacman/cmd/pacman-tools/packages.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

145 lines
5.3 KiB
Go

package main
// What the keeper reads from the machine into the bus (novox/hq ADR 0507 §2, §4, §7): the local package
// database (`pacman -Q`, `pacman -Qeq`) with each package's repository from the sync databases (`pacman -Sl`),
// and what a full upgrade would change (`checkupdates`). Every line is read against the shape of a package name
// and a version, and a line that is not one is left out, so nothing but names, versions and repositories ever
// reaches a value on the bus (ADR 0507 §1): never a command line, an environment or a message pacman printed.
// None of these commands names a package, so none names a foreign one either.
import (
"fmt"
"regexp"
"sort"
"strings"
)
var (
// packageName is what pacman allows in a package's name (makepkg's lint: alphanumerics and @._+-).
packageName = regexp.MustCompile(`^[a-z0-9@._+][a-z0-9@._+-]*$`)
// packageVersion is an epoch, a version and a release: alphanumerics and ._+:~-.
packageVersion = regexp.MustCompile(`^[A-Za-z0-9._+~][A-Za-z0-9._+:~-]*$`)
// repoName is a repository's section name in pacman.conf.
repoName = regexp.MustCompile(`^[A-Za-z0-9_-][A-Za-z0-9._-]*$`)
)
// nameVersion reads a `<name> <version>` line, refusing anything else.
func nameVersion(l string) (string, string, bool) {
f := strings.Fields(l)
if len(f) != 2 || !packageName.MatchString(f[0]) || !packageVersion.MatchString(f[1]) {
return "", "", false
}
return f[0], f[1], true
}
// ReadInstalled is the node's installed packages, sorted by name, each with its version, whether it was installed
// explicitly, and its repository or RepoForeign; and how many lines of pacman's output were not a package and
// were left out.
func (m *Machine) ReadInstalled() ([]InstalledPackage, int, error) {
all, err := m.Out("pacman", "-Q")
if err != nil {
return nil, 0, err
}
explicit, err := m.query("-Qeq")
if err != nil {
return nil, 0, err
}
synced, err := m.Out("pacman", "-Sl")
if err != nil {
return nil, 0, err
}
skipped := 0
ex := map[string]bool{}
for _, l := range lines(explicit) {
if n := strings.TrimSpace(l); packageName.MatchString(n) {
ex[n] = true
}
}
// A package in two repositories is the first's: `pacman -Sl` lists them in pacman.conf's order.
repo := map[string]string{}
for _, l := range lines(synced) {
f := strings.Fields(l)
if len(f) < 3 || !repoName.MatchString(f[0]) || !packageName.MatchString(f[1]) {
continue
}
if _, seen := repo[f[1]]; !seen {
repo[f[1]] = f[0]
}
}
pkgs := []InstalledPackage{}
for _, l := range lines(all) {
name, version, ok := nameVersion(l)
if !ok {
skipped++
continue
}
p := InstalledPackage{Name: name, Version: version, Explicit: ex[name], Repo: repo[name]}
if p.Repo == "" {
p.Repo = RepoForeign
}
pkgs = append(pkgs, p)
}
sort.Slice(pkgs, func(i, j int) bool { return pkgs[i].Name < pkgs[j].Name })
return pkgs, skipped, nil
}
var updateLine = regexp.MustCompile(`^(\S+) (\S+) -> (\S+)$`)
// UpdateList is what a full upgrade would change, sorted by name, from checkupdates: a copy of the sync
// databases refreshed apart from the machine's own, so asking never makes a partial upgrade possible. Its
// status 2 is nothing to do; any other failure is an error, never an empty list.
func (m *Machine) UpdateList() ([]VersionChange, error) {
r := m.Run(bg(), "checkupdates")
switch {
case r.Err == "ENOENT":
return nil, fmt.Errorf("checkupdates is not installed: it comes with pacman-contrib, which this module declares")
case r.Err == "" && r.Status == 2:
return []VersionChange{}, nil
case r.Err != "" || r.Status != 0:
return nil, failure("checkupdates", "checkupdates", r)
}
ups := []VersionChange{}
for _, l := range lines(r.Stdout) {
u := updateLine.FindStringSubmatch(strings.TrimSpace(l))
if u == nil || !packageName.MatchString(u[1]) || !packageVersion.MatchString(u[2]) || !packageVersion.MatchString(u[3]) {
continue
}
ups = append(ups, VersionChange{Name: u[1], From: u[2], To: u[3]})
}
sort.Slice(ups, func(i, j int) bool { return ups[i].Name < ups[j].Name })
return ups, nil
}
// DiffInstalled is what changed from one package list to the next, each list sorted by name: a package added,
// removed, or at another version. A change of reason or repository alone is no change of a version.
func DiffInstalled(before, after []InstalledPackage) Changes {
c := Changes{Added: []PackageVersion{}, Removed: []PackageVersion{}, Changed: []VersionChange{}}
was := map[string]string{}
for _, p := range before {
was[p.Name] = p.Version
}
is := map[string]bool{}
for _, p := range after {
is[p.Name] = true
v, ok := was[p.Name]
switch {
case !ok:
c.Added = append(c.Added, PackageVersion{p.Name, p.Version})
case v != p.Version:
c.Changed = append(c.Changed, VersionChange{p.Name, v, p.Version})
}
}
for _, p := range before {
if !is[p.Name] {
c.Removed = append(c.Removed, PackageVersion{p.Name, p.Version})
}
}
sort.Slice(c.Added, func(i, j int) bool { return c.Added[i].Name < c.Added[j].Name })
sort.Slice(c.Removed, func(i, j int) bool { return c.Removed[i].Name < c.Removed[j].Name })
sort.Slice(c.Changed, func(i, j int) bool { return c.Changed[i].Name < c.Changed[j].Name })
return c
}
// Empty says a change changed nothing.
func (c Changes) Empty() bool { return len(c.Added)+len(c.Removed)+len(c.Changed) == 0 }