Files
mesh-catalog/modules/pacman/cmd/pacman-tools/dkms.go
T

167 lines
5.6 KiB
Go

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/<release>/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, ""
}