Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion toolkit/tools/graphpkgfetcher/graphpkgfetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,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