Skip to content

Commit c8978ab

Browse files
authored
Fixed toolkit's handling of RPMs with epoch values in their name (#10629)
1 parent 4df9dbc commit c8978ab

4 files changed

Lines changed: 315 additions & 18 deletions

File tree

toolkit/tools/graphpkgfetcher/graphpkgfetcher.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,8 @@ func assignRPMPath(node *pkggraph.PkgNode, outDir string, resolvedPackages []str
553553
}
554554

555555
func rpmPackageToRPMPath(rpmPackage, outDir string) string {
556-
// Construct the rpm path of the cloned package.
556+
// Construct the RPM path of the cloned package.
557+
rpmPackage = rpm.StripEpochFromPackageFullQualifiedName(rpmPackage)
557558
rpmName := fmt.Sprintf("%s.rpm", rpmPackage)
558559
return filepath.Join(outDir, rpmName)
559560
}

toolkit/tools/internal/rpm/rpm.go

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,25 @@ const (
6363
)
6464

6565
const (
66-
installedRPMRegexRPMIndex = 1
67-
installedRPMRegexVersionIndex = 2
68-
installedRPMRegexArchIndex = 3
69-
installedRPMRegexExpectedMatches = 4
66+
packageFQNRegexMatchSubString = iota
67+
packageFQNRegexNameIndex = iota
68+
packageFQNRegexEpochIndex = iota
69+
packageFQNRegexVersionIndex = iota
70+
packageFQNRegexReleaseIndex = iota
71+
packageFQNRegexArchIndex = iota
72+
packageFQNRegexExtensionIndex = iota
73+
packageFQNRegexExpectedMatches = iota
74+
)
75+
76+
const (
77+
installedRPMRegexMatchSubString = iota
78+
installedRPMRegexRPMIndex = iota
79+
installedRPMRegexVersionIndex = iota
80+
installedRPMRegexArchIndex = iota
81+
installedRPMRegexExpectedMatches = iota
82+
)
7083

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

100+
// A full qualified RPM name contains the package name, epoch, version, release, architecture, and extension.
101+
// Optional fields:
102+
// - epoch,
103+
// - architecture.
104+
// - "rpm" extension.
105+
//
106+
// Sample match:
107+
//
108+
// pkg-name-0:1.2.3-4.azl3.x86_64.rpm
109+
//
110+
// Groups can be used to split it into:
111+
// - name: pkg-name
112+
// - epoch: 0
113+
// - version: 1.2.3
114+
// - release: 4.azl3
115+
// - architecture: x86_64
116+
// - extension: rpm
117+
packageFQNRegex = regexp.MustCompile(`^\s*(\S+[^-])-(?:(\d+):)?(\d[^-:_]*)-(\d+(?:[^-\s]*?))(?:\.(noarch|x86_64|aarch64|src))?(?:\.(rpm))?\s*$`)
118+
86119
// Output from 'rpm' prints installed RPMs in a line with the following format:
87120
//
88121
// D: ========== +++ [name]-([epoch]:)[version]-[release].[distribution] [architecture]-linux [hex_value]
@@ -187,19 +220,15 @@ func getMacroDirWithFallback(allowDefault bool) (macroDir string, err error) {
187220
func ExtractNameFromRPMPath(rpmFilePath string) (packageName string, err error) {
188221
baseName := filepath.Base(rpmFilePath)
189222

223+
matches := packageFQNRegex.FindStringSubmatch(baseName)
224+
190225
// If the path is invalid, return empty string. We consider any string that has at least 1 '-' characters valid.
191-
if !strings.Contains(baseName, "-") {
226+
if matches == nil {
192227
err = fmt.Errorf("invalid RPM file path (%s), can't extract name", rpmFilePath)
193228
return
194229
}
195230

196-
rpmFileSplit := strings.Split(baseName, "-")
197-
packageName = strings.Join(rpmFileSplit[:len(rpmFileSplit)-2], "-")
198-
if packageName == "" {
199-
err = fmt.Errorf("invalid RPM file path (%s), can't extract name", rpmFilePath)
200-
return
201-
}
202-
return
231+
return matches[packageFQNRegexNameIndex], nil
203232
}
204233

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

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

664+
// StripEpochFromPackageFullQualifiedName removes the epoch from a package full qualified name if it is present.
665+
// Example:
666+
//
667+
// "pkg-name-0:1.2.3-4.azl3.x86_64" -> "pkg-name-1.2.3-4.azl3.x86_64"
668+
func StripEpochFromPackageFullQualifiedName(packageFQN string) string {
669+
var packageFQNBuilder strings.Builder
670+
671+
matches := packageFQNRegex.FindStringSubmatch(packageFQN)
672+
if matches == nil {
673+
return packageFQN
674+
}
675+
676+
packageFQNBuilder.WriteString(matches[packageFQNRegexNameIndex])
677+
packageFQNBuilder.WriteString("-")
678+
679+
packageFQNBuilder.WriteString(matches[packageFQNRegexVersionIndex])
680+
packageFQNBuilder.WriteString("-")
681+
682+
packageFQNBuilder.WriteString(matches[packageFQNRegexReleaseIndex])
683+
684+
if matches[packageFQNRegexArchIndex] != "" {
685+
packageFQNBuilder.WriteString(".")
686+
packageFQNBuilder.WriteString(matches[packageFQNRegexArchIndex])
687+
}
688+
689+
if matches[packageFQNRegexExtensionIndex] != "" {
690+
packageFQNBuilder.WriteString(".")
691+
packageFQNBuilder.WriteString(matches[packageFQNRegexExtensionIndex])
692+
}
693+
694+
return packageFQNBuilder.String()
695+
}
696+
639697
// TestRPMFromSRPM builds an RPM from the given SRPM and runs its '%check' section SRPM file
640698
// but it does not generate any RPM packages.
641699
func TestRPMFromSRPM(srpmFile, outArch string, defines map[string]string) (err error) {

toolkit/tools/internal/rpm/rpm_test.go

Lines changed: 232 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ func TestConflictingPackageRegex(t *testing.T) {
470470
name: "perl with epoch",
471471
inputLine: "D: ========== +++ perl-4:5.34.1-489.cm2 x86_64-linux 0x0",
472472
expectedMatch: true,
473-
expectedOutput: "perl-5.34.1-489.cm2.x86_64",
473+
expectedOutput: "perl-4:5.34.1-489.cm2.x86_64",
474474
},
475475
{
476476
name: "systemd no epoch",
@@ -494,3 +494,234 @@ func TestConflictingPackageRegex(t *testing.T) {
494494
})
495495
}
496496
}
497+
498+
func TestPackageFQNRegexWithValidInput(t *testing.T) {
499+
tests := []struct {
500+
name string
501+
input string
502+
expectedGroups []string
503+
}{
504+
{
505+
name: "package with epoch and architecture",
506+
input: "pkg-name-0:1.2.3-4.azl3.x86_64.rpm",
507+
expectedGroups: []string{"pkg-name", "0", "1.2.3", "4.azl3", "x86_64", "rpm"},
508+
},
509+
{
510+
name: "package with epoch and architecture but no '.rpm' suffix",
511+
input: "pkg-name-0:1.2.3-4.azl3.x86_64",
512+
expectedGroups: []string{"pkg-name", "0", "1.2.3", "4.azl3", "x86_64", ""},
513+
},
514+
{
515+
name: "package without epoch, and architecture",
516+
input: "pkg-name-1.2.3-4.azl3.rpm",
517+
expectedGroups: []string{"pkg-name", "", "1.2.3", "4.azl3", "", "rpm"},
518+
},
519+
{
520+
name: "package with architecture but no epoch",
521+
input: "pkg-name-1.2.3-4.azl3.aarch64",
522+
expectedGroups: []string{"pkg-name", "", "1.2.3", "4.azl3", "aarch64", ""},
523+
},
524+
{
525+
name: "package with epoch but no architecture",
526+
input: "pkg-name-0:1.2.3-4.azl3",
527+
expectedGroups: []string{"pkg-name", "0", "1.2.3", "4.azl3", "", ""},
528+
},
529+
{
530+
name: "package without '.rpm' suffix",
531+
input: "pkg-name-1.2.3-4.azl3.x86_64",
532+
expectedGroups: []string{"pkg-name", "", "1.2.3", "4.azl3", "x86_64", ""},
533+
},
534+
{
535+
name: "package with version containing the '+' character",
536+
input: "pkg-name-1.2.3+4-4.azl3.x86_64.rpm",
537+
expectedGroups: []string{"pkg-name", "", "1.2.3+4", "4.azl3", "x86_64", "rpm"},
538+
},
539+
{
540+
name: "package with version containing the '~' character",
541+
input: "pkg-name-1.2.3~4-4.azl3.x86_64.rpm",
542+
expectedGroups: []string{"pkg-name", "", "1.2.3~4", "4.azl3", "x86_64", "rpm"},
543+
},
544+
{
545+
name: "package with release containing two '.' characters",
546+
input: "pkg-name-1.2.3-4.5.azl3.x86_64.rpm",
547+
expectedGroups: []string{"pkg-name", "", "1.2.3", "4.5.azl3", "x86_64", "rpm"},
548+
},
549+
{
550+
name: "package with release containing the '_' character",
551+
input: "pkg-name-1.2.3-45.az_l3.x86_64.rpm",
552+
expectedGroups: []string{"pkg-name", "", "1.2.3", "45.az_l3", "x86_64", "rpm"},
553+
},
554+
{
555+
name: "package with release containing the `~` character",
556+
input: "pkg-name-1.2.3-45.azl3~2.x86_64.rpm",
557+
expectedGroups: []string{"pkg-name", "", "1.2.3", "45.azl3~2", "x86_64", "rpm"},
558+
},
559+
{
560+
name: "package with double dash in name",
561+
input: "nvidia-container-toolkit-1.15.0-1.azl3.x86_64.rpm",
562+
expectedGroups: []string{"nvidia-container-toolkit", "", "1.15.0", "1.azl3", "x86_64", "rpm"},
563+
},
564+
{
565+
name: "package with underscore in release",
566+
input: "nvidia-container-toolkit-550.54.15-2_5.15.162.2.1.azl3.x86_64.rpm",
567+
expectedGroups: []string{"nvidia-container-toolkit", "", "550.54.15", "2_5.15.162.2.1.azl3", "x86_64", "rpm"},
568+
},
569+
}
570+
571+
for _, tt := range tests {
572+
t.Run(tt.name, func(t *testing.T) {
573+
matches := packageFQNRegex.FindStringSubmatch(tt.input)
574+
assert.NotNil(t, matches)
575+
assert.Equal(t, tt.expectedGroups, matches[1:])
576+
})
577+
}
578+
}
579+
580+
func TestPackageFQNRegexWithInvalidInput(t *testing.T) {
581+
tests := []struct {
582+
name string
583+
input string
584+
}{
585+
{
586+
name: "package with missing version",
587+
input: "pkg-name--4.azl3.x86_64.rpm",
588+
},
589+
{
590+
name: "package with missing release",
591+
input: "pkg-name-1.2.3-.azl3.x86_64.rpm",
592+
},
593+
{
594+
name: "package with missing name",
595+
input: "-1.2.3-4.azl3.x86_64.rpm",
596+
},
597+
{
598+
name: "package with only hyphen",
599+
input: "-",
600+
},
601+
{
602+
name: "package with version not beginning with a digit",
603+
input: "pkg-name-0:a1.2.3-4.azl3.x86_64.rpm",
604+
},
605+
{
606+
name: "package with release not beginning with a digit",
607+
input: "pkg-name-0:1.2.3-D4.azl3.x86_64.rpm",
608+
},
609+
{
610+
name: "package with epoch not beginning with a digit",
611+
input: "pkg-name-0:1.2.3-D4.azl3.x86_64.rpm",
612+
},
613+
{
614+
name: "package with epoch unsupported architecture",
615+
input: "pkg-name-0:1.2.3-D4.azl3.other_arch.rpm",
616+
},
617+
}
618+
619+
for _, tt := range tests {
620+
t.Run(tt.name, func(t *testing.T) {
621+
matches := packageFQNRegex.FindStringSubmatch(tt.input)
622+
assert.Nil(t, matches)
623+
})
624+
}
625+
}
626+
627+
func TestStripEpochFromPackageFullQualifiedNameWithValidInput(t *testing.T) {
628+
tests := []struct {
629+
name string
630+
input string
631+
expected string
632+
}{
633+
{
634+
name: "package with epoch and architecture",
635+
input: "pkg-name-0:1.2.3-4.azl3.x86_64.rpm",
636+
expected: "pkg-name-1.2.3-4.azl3.x86_64.rpm",
637+
},
638+
{
639+
name: "package with epoch and architecture but no '.rpm' suffix",
640+
input: "pkg-name-0:1.2.3-4.azl3.x86_64",
641+
expected: "pkg-name-1.2.3-4.azl3.x86_64",
642+
},
643+
{
644+
name: "package with epoch but no architecture",
645+
input: "pkg-name-0:1.2.3-4.azl3",
646+
expected: "pkg-name-1.2.3-4.azl3",
647+
},
648+
{
649+
name: "package with architecture but no epoch",
650+
input: "pkg-name-1.2.3-4.azl3.aarch64",
651+
expected: "pkg-name-1.2.3-4.azl3.aarch64",
652+
},
653+
{
654+
name: "package without epoch, and architecture",
655+
input: "pkg-name-1.2.3-4.azl3.rpm",
656+
expected: "pkg-name-1.2.3-4.azl3.rpm",
657+
},
658+
{
659+
name: "package with version containing the '+' character",
660+
input: "pkg-name-1.2.3+4-4.azl3.x86_64.rpm",
661+
expected: "pkg-name-1.2.3+4-4.azl3.x86_64.rpm",
662+
},
663+
{
664+
name: "package with version containing the '~' character",
665+
input: "pkg-name-1.2.3~4-4.azl3.x86_64.rpm",
666+
expected: "pkg-name-1.2.3~4-4.azl3.x86_64.rpm",
667+
},
668+
{
669+
name: "package with release containing two '.' characters",
670+
input: "pkg-name-1.2.3-4.5.azl3.x86_64.rpm",
671+
expected: "pkg-name-1.2.3-4.5.azl3.x86_64.rpm",
672+
},
673+
{
674+
name: "package with release containing the '_' character",
675+
input: "pkg-name-1.2.3-4_5.azl3.x86_64.rpm",
676+
expected: "pkg-name-1.2.3-4_5.azl3.x86_64.rpm",
677+
},
678+
{
679+
name: "package with release containing the `~` character",
680+
input: "pkg-name-1.2.3-4~5.azl3.x86_64.rpm",
681+
expected: "pkg-name-1.2.3-4~5.azl3.x86_64.rpm",
682+
},
683+
}
684+
685+
for _, tt := range tests {
686+
t.Run(tt.name, func(t *testing.T) {
687+
actual := StripEpochFromPackageFullQualifiedName(tt.input)
688+
assert.Equal(t, tt.expected, actual)
689+
})
690+
}
691+
}
692+
693+
func TestStripEpochFromPackageFullQualifiedNameWithInvalidInput(t *testing.T) {
694+
tests := []struct {
695+
name string
696+
input string
697+
expected string
698+
}{
699+
{
700+
name: "invalid package name",
701+
input: "invalid-package-name",
702+
expected: "invalid-package-name",
703+
},
704+
{
705+
name: "empty package name",
706+
input: "",
707+
expected: "",
708+
},
709+
{
710+
name: "package name with only hyphens",
711+
input: "----",
712+
expected: "----",
713+
},
714+
{
715+
name: "package name with spaces",
716+
input: "pkg name-1.2.3-4.azl3.x86_64.rpm",
717+
expected: "pkg name-1.2.3-4.azl3.x86_64.rpm",
718+
},
719+
}
720+
721+
for _, tt := range tests {
722+
t.Run(tt.name, func(t *testing.T) {
723+
actual := StripEpochFromPackageFullQualifiedName(tt.input)
724+
assert.Equal(t, tt.expected, actual)
725+
})
726+
}
727+
}

0 commit comments

Comments
 (0)