Skip to content

Commit

Permalink
Fixed toolkit's handling of RPMs with epoch values in their name (#10629
Browse files Browse the repository at this point in the history
)
  • Loading branch information
PawelWMS authored Oct 5, 2024
1 parent 4df9dbc commit c8978ab
Show file tree
Hide file tree
Showing 4 changed files with 315 additions and 18 deletions.
3 changes: 2 additions & 1 deletion toolkit/tools/graphpkgfetcher/graphpkgfetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,8 @@ func assignRPMPath(node *pkggraph.PkgNode, outDir string, resolvedPackages []str
}

func rpmPackageToRPMPath(rpmPackage, outDir string) string {
// Construct the rpm path of the cloned package.
// Construct the RPM path of the cloned package.
rpmPackage = rpm.StripEpochFromPackageFullQualifiedName(rpmPackage)
rpmName := fmt.Sprintf("%s.rpm", rpmPackage)
return filepath.Join(outDir, rpmName)
}
Expand Down
90 changes: 74 additions & 16 deletions toolkit/tools/internal/rpm/rpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,25 @@ const (
)

const (
installedRPMRegexRPMIndex = 1
installedRPMRegexVersionIndex = 2
installedRPMRegexArchIndex = 3
installedRPMRegexExpectedMatches = 4
packageFQNRegexMatchSubString = iota
packageFQNRegexNameIndex = iota
packageFQNRegexEpochIndex = iota
packageFQNRegexVersionIndex = iota
packageFQNRegexReleaseIndex = iota
packageFQNRegexArchIndex = iota
packageFQNRegexExtensionIndex = iota
packageFQNRegexExpectedMatches = iota
)

const (
installedRPMRegexMatchSubString = iota
installedRPMRegexRPMIndex = iota
installedRPMRegexVersionIndex = iota
installedRPMRegexArchIndex = iota
installedRPMRegexExpectedMatches = iota
)

const (
rpmProgram = "rpm"
rpmSpecProgram = "rpmspec"
rpmBuildProgram = "rpmbuild"
Expand All @@ -83,6 +97,25 @@ var (
// It works multi-line strings containing the whole file content, thus the need for the 'm' flag.
checkSectionRegex = regexp.MustCompile(`(?m)^\s*%check`)

// A full qualified RPM name contains the package name, epoch, version, release, architecture, and extension.
// Optional fields:
// - epoch,
// - architecture.
// - "rpm" extension.
//
// Sample match:
//
// pkg-name-0:1.2.3-4.azl3.x86_64.rpm
//
// Groups can be used to split it into:
// - name: pkg-name
// - epoch: 0
// - version: 1.2.3
// - release: 4.azl3
// - architecture: x86_64
// - extension: rpm
packageFQNRegex = regexp.MustCompile(`^\s*(\S+[^-])-(?:(\d+):)?(\d[^-:_]*)-(\d+(?:[^-\s]*?))(?:\.(noarch|x86_64|aarch64|src))?(?:\.(rpm))?\s*$`)

// Output from 'rpm' prints installed RPMs in a line with the following format:
//
// D: ========== +++ [name]-([epoch]:)[version]-[release].[distribution] [architecture]-linux [hex_value]
Expand Down Expand Up @@ -187,19 +220,15 @@ func getMacroDirWithFallback(allowDefault bool) (macroDir string, err error) {
func ExtractNameFromRPMPath(rpmFilePath string) (packageName string, err error) {
baseName := filepath.Base(rpmFilePath)

matches := packageFQNRegex.FindStringSubmatch(baseName)

// If the path is invalid, return empty string. We consider any string that has at least 1 '-' characters valid.
if !strings.Contains(baseName, "-") {
if matches == nil {
err = fmt.Errorf("invalid RPM file path (%s), can't extract name", rpmFilePath)
return
}

rpmFileSplit := strings.Split(baseName, "-")
packageName = strings.Join(rpmFileSplit[:len(rpmFileSplit)-2], "-")
if packageName == "" {
err = fmt.Errorf("invalid RPM file path (%s), can't extract name", rpmFilePath)
return
}
return
return matches[packageFQNRegexNameIndex], nil
}

// getCommonBuildArgs will generate arguments to pass to 'rpmbuild'.
Expand Down Expand Up @@ -526,10 +555,6 @@ func extractCompetingPackageInfoFromLine(line string) (match bool, pkgName strin
pkgName := matches[installedRPMRegexRPMIndex]
version := matches[installedRPMRegexVersionIndex]
arch := matches[installedRPMRegexArchIndex]
// Names should not contain the epoch, strip everything before the ":"" in the string. "Version": "0:1.2-3", becomes "1.2-3"
if strings.Contains(version, ":") {
version = strings.Split(version, ":")[1]
}

return true, fmt.Sprintf("%s-%s.%s", pkgName, version, arch)
}
Expand Down Expand Up @@ -636,6 +661,39 @@ func BuildCompatibleSpecsList(baseDir string, inputSpecPaths []string, defines m
return filterCompatibleSpecs(specPaths, defines)
}

// StripEpochFromPackageFullQualifiedName removes the epoch from a package full qualified name if it is present.
// Example:
//
// "pkg-name-0:1.2.3-4.azl3.x86_64" -> "pkg-name-1.2.3-4.azl3.x86_64"
func StripEpochFromPackageFullQualifiedName(packageFQN string) string {
var packageFQNBuilder strings.Builder

matches := packageFQNRegex.FindStringSubmatch(packageFQN)
if matches == nil {
return packageFQN
}

packageFQNBuilder.WriteString(matches[packageFQNRegexNameIndex])
packageFQNBuilder.WriteString("-")

packageFQNBuilder.WriteString(matches[packageFQNRegexVersionIndex])
packageFQNBuilder.WriteString("-")

packageFQNBuilder.WriteString(matches[packageFQNRegexReleaseIndex])

if matches[packageFQNRegexArchIndex] != "" {
packageFQNBuilder.WriteString(".")
packageFQNBuilder.WriteString(matches[packageFQNRegexArchIndex])
}

if matches[packageFQNRegexExtensionIndex] != "" {
packageFQNBuilder.WriteString(".")
packageFQNBuilder.WriteString(matches[packageFQNRegexExtensionIndex])
}

return packageFQNBuilder.String()
}

// TestRPMFromSRPM builds an RPM from the given SRPM and runs its '%check' section SRPM file
// but it does not generate any RPM packages.
func TestRPMFromSRPM(srpmFile, outArch string, defines map[string]string) (err error) {
Expand Down
233 changes: 232 additions & 1 deletion toolkit/tools/internal/rpm/rpm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ func TestConflictingPackageRegex(t *testing.T) {
name: "perl with epoch",
inputLine: "D: ========== +++ perl-4:5.34.1-489.cm2 x86_64-linux 0x0",
expectedMatch: true,
expectedOutput: "perl-5.34.1-489.cm2.x86_64",
expectedOutput: "perl-4:5.34.1-489.cm2.x86_64",
},
{
name: "systemd no epoch",
Expand All @@ -494,3 +494,234 @@ func TestConflictingPackageRegex(t *testing.T) {
})
}
}

func TestPackageFQNRegexWithValidInput(t *testing.T) {
tests := []struct {
name string
input string
expectedGroups []string
}{
{
name: "package with epoch and architecture",
input: "pkg-name-0:1.2.3-4.azl3.x86_64.rpm",
expectedGroups: []string{"pkg-name", "0", "1.2.3", "4.azl3", "x86_64", "rpm"},
},
{
name: "package with epoch and architecture but no '.rpm' suffix",
input: "pkg-name-0:1.2.3-4.azl3.x86_64",
expectedGroups: []string{"pkg-name", "0", "1.2.3", "4.azl3", "x86_64", ""},
},
{
name: "package without epoch, and architecture",
input: "pkg-name-1.2.3-4.azl3.rpm",
expectedGroups: []string{"pkg-name", "", "1.2.3", "4.azl3", "", "rpm"},
},
{
name: "package with architecture but no epoch",
input: "pkg-name-1.2.3-4.azl3.aarch64",
expectedGroups: []string{"pkg-name", "", "1.2.3", "4.azl3", "aarch64", ""},
},
{
name: "package with epoch but no architecture",
input: "pkg-name-0:1.2.3-4.azl3",
expectedGroups: []string{"pkg-name", "0", "1.2.3", "4.azl3", "", ""},
},
{
name: "package without '.rpm' suffix",
input: "pkg-name-1.2.3-4.azl3.x86_64",
expectedGroups: []string{"pkg-name", "", "1.2.3", "4.azl3", "x86_64", ""},
},
{
name: "package with version containing the '+' character",
input: "pkg-name-1.2.3+4-4.azl3.x86_64.rpm",
expectedGroups: []string{"pkg-name", "", "1.2.3+4", "4.azl3", "x86_64", "rpm"},
},
{
name: "package with version containing the '~' character",
input: "pkg-name-1.2.3~4-4.azl3.x86_64.rpm",
expectedGroups: []string{"pkg-name", "", "1.2.3~4", "4.azl3", "x86_64", "rpm"},
},
{
name: "package with release containing two '.' characters",
input: "pkg-name-1.2.3-4.5.azl3.x86_64.rpm",
expectedGroups: []string{"pkg-name", "", "1.2.3", "4.5.azl3", "x86_64", "rpm"},
},
{
name: "package with release containing the '_' character",
input: "pkg-name-1.2.3-45.az_l3.x86_64.rpm",
expectedGroups: []string{"pkg-name", "", "1.2.3", "45.az_l3", "x86_64", "rpm"},
},
{
name: "package with release containing the `~` character",
input: "pkg-name-1.2.3-45.azl3~2.x86_64.rpm",
expectedGroups: []string{"pkg-name", "", "1.2.3", "45.azl3~2", "x86_64", "rpm"},
},
{
name: "package with double dash in name",
input: "nvidia-container-toolkit-1.15.0-1.azl3.x86_64.rpm",
expectedGroups: []string{"nvidia-container-toolkit", "", "1.15.0", "1.azl3", "x86_64", "rpm"},
},
{
name: "package with underscore in release",
input: "nvidia-container-toolkit-550.54.15-2_5.15.162.2.1.azl3.x86_64.rpm",
expectedGroups: []string{"nvidia-container-toolkit", "", "550.54.15", "2_5.15.162.2.1.azl3", "x86_64", "rpm"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
matches := packageFQNRegex.FindStringSubmatch(tt.input)
assert.NotNil(t, matches)
assert.Equal(t, tt.expectedGroups, matches[1:])
})
}
}

func TestPackageFQNRegexWithInvalidInput(t *testing.T) {
tests := []struct {
name string
input string
}{
{
name: "package with missing version",
input: "pkg-name--4.azl3.x86_64.rpm",
},
{
name: "package with missing release",
input: "pkg-name-1.2.3-.azl3.x86_64.rpm",
},
{
name: "package with missing name",
input: "-1.2.3-4.azl3.x86_64.rpm",
},
{
name: "package with only hyphen",
input: "-",
},
{
name: "package with version not beginning with a digit",
input: "pkg-name-0:a1.2.3-4.azl3.x86_64.rpm",
},
{
name: "package with release not beginning with a digit",
input: "pkg-name-0:1.2.3-D4.azl3.x86_64.rpm",
},
{
name: "package with epoch not beginning with a digit",
input: "pkg-name-0:1.2.3-D4.azl3.x86_64.rpm",
},
{
name: "package with epoch unsupported architecture",
input: "pkg-name-0:1.2.3-D4.azl3.other_arch.rpm",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
matches := packageFQNRegex.FindStringSubmatch(tt.input)
assert.Nil(t, matches)
})
}
}

func TestStripEpochFromPackageFullQualifiedNameWithValidInput(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "package with epoch and architecture",
input: "pkg-name-0:1.2.3-4.azl3.x86_64.rpm",
expected: "pkg-name-1.2.3-4.azl3.x86_64.rpm",
},
{
name: "package with epoch and architecture but no '.rpm' suffix",
input: "pkg-name-0:1.2.3-4.azl3.x86_64",
expected: "pkg-name-1.2.3-4.azl3.x86_64",
},
{
name: "package with epoch but no architecture",
input: "pkg-name-0:1.2.3-4.azl3",
expected: "pkg-name-1.2.3-4.azl3",
},
{
name: "package with architecture but no epoch",
input: "pkg-name-1.2.3-4.azl3.aarch64",
expected: "pkg-name-1.2.3-4.azl3.aarch64",
},
{
name: "package without epoch, and architecture",
input: "pkg-name-1.2.3-4.azl3.rpm",
expected: "pkg-name-1.2.3-4.azl3.rpm",
},
{
name: "package with version containing the '+' character",
input: "pkg-name-1.2.3+4-4.azl3.x86_64.rpm",
expected: "pkg-name-1.2.3+4-4.azl3.x86_64.rpm",
},
{
name: "package with version containing the '~' character",
input: "pkg-name-1.2.3~4-4.azl3.x86_64.rpm",
expected: "pkg-name-1.2.3~4-4.azl3.x86_64.rpm",
},
{
name: "package with release containing two '.' characters",
input: "pkg-name-1.2.3-4.5.azl3.x86_64.rpm",
expected: "pkg-name-1.2.3-4.5.azl3.x86_64.rpm",
},
{
name: "package with release containing the '_' character",
input: "pkg-name-1.2.3-4_5.azl3.x86_64.rpm",
expected: "pkg-name-1.2.3-4_5.azl3.x86_64.rpm",
},
{
name: "package with release containing the `~` character",
input: "pkg-name-1.2.3-4~5.azl3.x86_64.rpm",
expected: "pkg-name-1.2.3-4~5.azl3.x86_64.rpm",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := StripEpochFromPackageFullQualifiedName(tt.input)
assert.Equal(t, tt.expected, actual)
})
}
}

func TestStripEpochFromPackageFullQualifiedNameWithInvalidInput(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "invalid package name",
input: "invalid-package-name",
expected: "invalid-package-name",
},
{
name: "empty package name",
input: "",
expected: "",
},
{
name: "package name with only hyphens",
input: "----",
expected: "----",
},
{
name: "package name with spaces",
input: "pkg name-1.2.3-4.azl3.x86_64.rpm",
expected: "pkg name-1.2.3-4.azl3.x86_64.rpm",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := StripEpochFromPackageFullQualifiedName(tt.input)
assert.Equal(t, tt.expected, actual)
})
}
}
Loading

0 comments on commit c8978ab

Please sign in to comment.