package main // The DKMS gate (novox/hq ADR 0508 ยง3): before the transaction, every kernel package the upgrade would change is // compared with the kernel bound each installed DKMS module declares itself: `Linux-Maximum` in its source's // META, or `BUILD_EXCLUSIVE_KERNEL` (a pattern of kernel releases) in its dkms.conf. A kernel past a bound is // left out of the run with its headers, and the rest is upgraded. A kernel package is known by its files, not // its name: one whose files hold usr/lib/modules//vmlinuz. import ( "encoding/json" "fmt" "regexp" "strconv" "strings" ) var ( vmlinuzPath = regexp.MustCompile(`^/?usr/lib/modules/([^/]+)/vmlinuz$`) linuxMaximum = regexp.MustCompile(`(?m)^Linux-Maximum:\s*(\S+)\s*$`) buildExclusive = regexp.MustCompile(`(?m)^BUILD_EXCLUSIVE_KERNEL=(?:"([^"]*)"|'([^']*)'|(\S+))\s*$`) leadingNumber = regexp.MustCompile(`^[0-9]+(\.[0-9]+)*`) ) // kernelRelease is the release an installed kernel package's image is for, and whether the package is a kernel. func (m *Machine) kernelRelease(pkg string) (string, bool, error) { out, err := m.Out("pacman", "-Qlq", "--", pkg) if err != nil { return "", false, fmt.Errorf("the files of %s: %w", pkg, err) } for _, l := range lines(out) { if g := vmlinuzPath.FindStringSubmatch(strings.TrimSpace(l)); g != nil { return g[1], true, nil } } return "", false, nil } // NewRelease is the release a kernel package's new version will install, derived from it as the installed // release is from the installed version: `7.2.8.arch1-2` installs `7.2.8-arch1-2`, `6.18.54-2` (the LTS // kernel) installs `6.18.54-2-lts`. func NewRelease(installedVersion, installedRelease, newVersion string) string { base := func(v string) string { return strings.Replace(v, ".arch", "-arch", 1) } suffix := "" if b := base(installedVersion); strings.HasPrefix(installedRelease, b) { suffix = strings.TrimPrefix(installedRelease, b) } return base(newVersion) + suffix } // pastMaximum says whether a release is past a Linux-Maximum bound, compared on as many numbers as the bound // gives: 7.3.1 is past 7.2, 7.2.9 is not. func pastMaximum(release, maximum string) bool { bound := strings.Split(leadingNumber.FindString(maximum), ".") have := strings.Split(leadingNumber.FindString(release), ".") if bound[0] == "" || have[0] == "" { return false } for i, b := range bound { bn, _ := strconv.Atoi(b) hn := 0 if i < len(have) { hn, _ = strconv.Atoi(have[i]) } if hn != bn { return hn > bn } } return false } // dkmsBound is the bound one DKMS module declares, read from its source. type dkmsBound struct { Maximum string Exclusive string } func (m *Machine) dkmsBound(mod DKMSModule) dkmsBound { dir := "/usr/src/" + mod.Module + "-" + mod.Version + "/" var b dkmsBound if meta, err := m.ReadFile(dir + "META"); err == nil { if g := linuxMaximum.FindSubmatch(meta); g != nil { b.Maximum = string(g[1]) } } if conf, err := m.ReadFile(dir + "dkms.conf"); err == nil { if g := buildExclusive.FindSubmatch(conf); g != nil { b.Exclusive = string(g[1]) + string(g[2]) + string(g[3]) } } return b } // heldBy says which bound of a module holds a release, or nothing. func heldBy(b dkmsBound, release string) (string, error) { if b.Maximum != "" && pastMaximum(release, b.Maximum) { return "Linux-Maximum: " + b.Maximum, nil } if b.Exclusive != "" { re, err := regexp.Compile(b.Exclusive) if err != nil { return "", fmt.Errorf("BUILD_EXCLUSIVE_KERNEL %q is not a pattern this reads: %w", b.Exclusive, err) } if !re.MatchString(release) { return "BUILD_EXCLUSIVE_KERNEL=" + b.Exclusive, nil } } return "", nil } // Gate is the DKMS gate for the node's next run: the kernels it holds, the packages left out of the transaction // for them (each held kernel and its headers), or why the run cannot start. func (u *Upgrades) Gate() (held []HeldKernel, ignore []string, why string) { e, err := u.Updates.Get(u.Node) if err != nil { return nil, nil, "what an upgrade would change could not be read from the bus: " + err.Error() } if e == nil { return nil, nil, "what an upgrade would change is not on the bus yet, so no kernel it would bring can be checked" } var updates UpdatesValue if err := json.Unmarshal(e.Value, &updates); err != nil { return nil, nil, "what an upgrade would change is not readable: " + err.Error() } if !updates.Reachable { return nil, nil, "the node could not reach its mirrors, so what an upgrade would change is not known" } var modules []DKMSModule var bounds []dkmsBound held, ignore = []HeldKernel{}, []string{} for _, p := range updates.Packages { if p.Name != "linux" && !strings.HasPrefix(p.Name, "linux-") { continue } release, kernel, err := u.Machine.kernelRelease(p.Name) if err != nil { return nil, nil, "whether " + p.Name + " is a kernel could not be read: " + err.Error() } if !kernel { continue } if modules == nil { if modules, err = u.Machine.DKMSModules(); err != nil { return nil, nil, "the DKMS modules could not be read: " + err.Error() } for _, mod := range modules { b := u.Machine.dkmsBound(mod) bounds = append(bounds, b) } } next := NewRelease(p.From, release, p.To) for i, mod := range modules { bound, err := heldBy(bounds[i], next) if err != nil { return nil, nil, mod.Module + " " + mod.Version + ": " + err.Error() } if bound == "" { continue } held = append(held, HeldKernel{Package: p.Name, Version: p.To, Module: mod.Module + "/" + mod.Version, Bound: bound}) if !contains(ignore, p.Name) { ignore = append(ignore, p.Name, p.Name+"-headers") } } } return held, ignore, "" }