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 ` ` 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 }