blob: 98cd279e06feeadebf13f6e838190b01bafccc13 [file] [log] [blame] [edit]
package dkms
import (
"context"
"fmt"
"os"
"os/exec"
"path"
"strings"
"cos.googlesource.com/cos/tools.git/src/pkg/fs"
"cos.googlesource.com/cos/tools.git/src/pkg/gcs"
"github.com/golang/glog"
)
// CompareVersions compares two kernel module or package versions and returns
// -1 if the first is less than the second, 0 if they are equal, and 1 if the
// first is greater than the second.
//
// Specifically, it pads each number in the version strings with leading 0s so
// that all numbers have at least 3 digits, then compares them in alphabetical
// order. While this choice of comparison is arbitrary, this is precisely what
// standard DKMS does. Because kernel module version strings do not follow a
// standard format like semver, any choice of comparison is arbitrary, and
// this at least handles common cases well.
//
// This is not compatible with semver version comparison. Specifically,
// the way pre-release versions are compared differs, though the
// major/minor/patch version comparison should be the same in most cases.
func CompareVersions(a, b string) int {
return strings.Compare(padDigits(a, 3), padDigits(b, 3))
}
// padDigits pads all of the numbers in a string to contain at least
// padNumber digits.
func padDigits(s string, padNumber int) string {
// Going from right to left, we keep track of how many digits we've
// seen in the current run of digits. If we see a non-digit before
// we've seen padNumber digits, we insert enough 0s to increase
// the number of digits to padNumber. We then reset our digitCount
// to 0 and continue.
digitCount := 0
var reversedChars []byte
for i := len(s) - 1; i >= 0; i-- {
c := s[i]
if c >= '0' && c <= '9' {
digitCount += 1
} else {
for j := digitCount; j < padNumber; j++ {
reversedChars = append(reversedChars, '0')
}
digitCount = 0
}
reversedChars = append(reversedChars, c)
}
// If the last character we looked at was a digit, it needs to
// be padded too.
for j := digitCount; j < padNumber; j++ {
reversedChars = append(reversedChars, '0')
}
var chars []byte
for i := len(reversedChars) - 1; i >= 0; i-- {
chars = append(chars, reversedChars[i])
}
result := string(chars)
return result
}
// ModuleVersion returns the version of a compiled module based on the version
// string output by modinfo.
func ModuleVersion(moduleNameOrPath string) (string, error) {
cmd := exec.Command("modinfo", "-F", "version", moduleNameOrPath)
out, err := cmd.CombinedOutput()
if err != nil {
glog.Error(string(out))
return "", fmt.Errorf("error running command %v: %v", cmd, err)
}
if len(out) == 0 {
return "", fmt.Errorf("could not find version for %s in modinfo", moduleNameOrPath)
}
return strings.TrimSpace(string(out)), nil
}
// CompatiblePackageVersions checks the local DKMS tree for non-broken
// packages which have the same field values as a provided package except for
// the Version, and returns the version of each compatible package.
func CompatiblePackageVersions(pkg *Package) ([]string, error) {
var versions []string
packageDir := path.Join(pkg.Trees.Dkms, pkg.Name)
if !fs.IsDir(packageDir) {
return versions, nil
}
entries, err := os.ReadDir(packageDir)
if err != nil {
// If the package directory doesn't exist locally, it just means that the
// package is not yet built locally, which is not an error.
if os.IsNotExist(err) {
return versions, nil
} else {
return nil, fmt.Errorf("couldn't read package dkms tree at %s: %v", packageDir, err)
}
}
for _, entry := range entries {
candidateVersion := entry.Name()
// Create a copy of package with the candidate version; if that
// package exists and is not broken, then it is compatible with
// the current system
candidatePackage := pkg
candidatePackage.Version = candidateVersion
if Status(candidatePackage) != Broken {
versions = append(versions, candidateVersion)
}
}
return versions, nil
}
// CachedCompatiblePackageVersions checks the DKMS tree in a GCS bucket for
// non-broken packages which have the same field values as a provided package
// except for the Version, and returns the version of each compatible package.
func CachedCompatiblePackageVersions(ctx context.Context, pkg *Package, cache *gcs.GCSBucket) ([]string, error) {
var versions []string
packageUri := cache.URI(pkg.Name)
candidatePaths, err := cache.ListDir(ctx, pkg.Name)
if err != nil {
return nil, fmt.Errorf("couldn't read cached package dkms tree at %s: %v", packageUri, err)
}
for _, candidatePath := range candidatePaths {
// Create a copy of package with the candidate version; if that
// package exists and is not broken, then it is compatible with
// the current system
candidatePath = path.Clean(candidatePath) // remove trailing slash
_, candidateVersion := path.Split(candidatePath)
candidatePackage := pkg
candidatePackage.Version = candidateVersion
if CachedStatus(ctx, candidatePackage, cache) != Broken {
versions = append(versions, candidateVersion)
}
}
return versions, nil
}