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"` } var updateLine = regexp.MustCompile(`^(\S+) (\S+) -> (\S+)`) // Updates is what a full upgrade would change, from checkupdates: a copy of the sync databases // refreshed apart from the machine's own, so asking never makes a partial upgrade possible. func (m *Machine) Updates() (map[string]any, 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 map[string]any{"count": 0, "updates": []Update{}}, nil case r.Err != "" || r.Status != 0: return nil, failure("checkupdates", "checkupdates", r) } ups := []Update{} for _, l := range lines(r.Stdout) { if u := updateLine.FindStringSubmatch(strings.TrimSpace(l)); u != nil { ups = append(ups, Update{u[1], u[2], u[3]}) } } 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 }