| 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 |
| } |
| |