| 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 | |
| } | |
| // VersionInRange returns true if a version is greater than or equal to | |
| // the min version and less than or equal to the max version. | |
| // | |
| // One or both ends of the range may be omitted, in which case, the | |
| // corresponding check(s) are skipped. | |
| func VersionInRange(version, minVersion, maxVersion string) bool { | |
| if minVersion != "" && CompareVersions(version, minVersion) < 0 { | |
| return false | |
| } | |
| if maxVersion != "" && CompareVersions(version, maxVersion) > 0 { | |
| return false | |
| } | |
| return true | |
| } |