1
1
mirror of https://github.com/goreleaser/nfpm synced 2026-07-23 05:54:49 +02:00
Files
nfpm/rpm/srpm_test.go
Carlos Alexandro Becker d20c0c9937 fix(rpm): escape % in generated SRPM spec so scriptlets survive rebuild (#1109)
User content (scriptlets, %description, summary, file paths and changelog)
was inlined verbatim into the generated .spec file. rpmbuild reinterprets %
when the SRPM is rebuilt: %% collapses to a single %, %{macro} expands, and
%(command) executes at build time. The common longest-suffix-strip idiom
${VAR%%.*} silently became ${VAR%.*} (shortest suffix) in install scriptlets,
with no warning.

Double every % in user-controlled spec text via escapeSpecText so rpmbuild
restores it literally on rebuild. Binary RPMs are unaffected -- their
scriptlets go straight into header tags with no spec parsing -- so this is
scoped to the SRPM path.

Add a unit test asserting the escaped output, and extend the srpm rebuild
acceptance test with a ${host%%.*} scriptlet that must survive the real
rpmbuild --rebuild.

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-02 14:48:34 -03:00

249 lines
8.4 KiB
Go

package rpm
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/goreleaser/nfpm/v2"
"github.com/goreleaser/nfpm/v2/files"
"github.com/sassoftware/go-rpmutils"
"github.com/stretchr/testify/require"
)
func TestSRPMConventionalExtension(t *testing.T) {
require.Equal(t, ".src.rpm", DefaultSRPM.ConventionalExtension())
}
func TestSRPMConventionalFileName(t *testing.T) {
info := exampleInfo()
// Source packages are named without an architecture component.
require.Equal(t, "foo-1.0.0-1.src.rpm", DefaultSRPM.ConventionalFileName(info))
}
func TestSRPM(t *testing.T) {
var buf bytes.Buffer
require.NoError(t, DefaultSRPM.Package(exampleInfo(), &buf))
rpm, err := rpmutils.ReadRpm(bytes.NewReader(buf.Bytes()))
require.NoError(t, err)
// A source package uses arch "src" regardless of the configured target arch.
arch, err := rpm.Header.GetString(rpmutils.ARCH)
require.NoError(t, err)
require.Equal(t, "src", arch)
osName, err := rpm.Header.GetString(rpmutils.OS)
require.NoError(t, err)
require.Equal(t, "linux", osName)
version, err := rpm.Header.GetString(rpmutils.VERSION)
require.NoError(t, err)
require.Equal(t, "1.0.0", version)
release, err := rpm.Header.GetString(rpmutils.RELEASE)
require.NoError(t, err)
require.Equal(t, "1", release)
group, err := rpm.Header.GetString(rpmutils.GROUP)
require.NoError(t, err)
require.Equal(t, "foo", group)
summary, err := rpm.Header.GetString(rpmutils.SUMMARY)
require.NoError(t, err)
require.Equal(t, "Foo does things", summary)
// It is marked as a source package and omits SOURCERPM.
srcPkg, err := rpm.Header.GetUint32s(tagSourcePackage)
require.NoError(t, err)
require.Equal(t, []uint32{1}, srcPkg)
_, err = rpm.Header.GetString(rpmutils.SOURCERPM)
require.Error(t, err, "source packages must not set SOURCERPM")
// The payload contains the generated spec and the bundled source tarball.
tree := getTree(t, buf.Bytes())
require.Contains(t, tree, "/foo.spec")
require.Contains(t, tree, "/foo-1.0.0.tar.gz")
}
func TestSRPMArchAlwaysSrc(t *testing.T) {
info := exampleInfo()
info.Arch = "riscv64"
var buf bytes.Buffer
require.NoError(t, DefaultSRPM.Package(info, &buf))
rpm, err := rpmutils.ReadRpm(bytes.NewReader(buf.Bytes()))
require.NoError(t, err)
arch, err := rpm.Header.GetString(rpmutils.ARCH)
require.NoError(t, err)
require.Equal(t, "src", arch)
// The target architecture is carried by the spec's BuildArch instead.
spec, err := extractFileFromRpm(buf.Bytes(), "/foo.spec")
require.NoError(t, err)
require.Contains(t, string(spec), "BuildArch: "+archToRPM["riscv64"])
}
func TestSRPMSpecContents(t *testing.T) {
var buf bytes.Buffer
require.NoError(t, DefaultSRPM.Package(exampleInfo(), &buf))
specBytes, err := extractFileFromRpm(buf.Bytes(), "/foo.spec")
require.NoError(t, err)
spec := string(specBytes)
// Metadata preamble.
require.Contains(t, spec, "Name: foo")
require.Contains(t, spec, "Version: 1.0.0")
require.Contains(t, spec, "Release: 1")
require.Contains(t, spec, "Epoch: 0")
require.Contains(t, spec, "BuildArch: "+archToRPM["amd64"])
require.Contains(t, spec, "License: MIT")
require.Contains(t, spec, "Group: foo")
// Dependencies.
require.Contains(t, spec, "Requires: bash")
require.Contains(t, spec, "Provides: bzr")
require.Contains(t, spec, "Conflicts: zsh")
require.Contains(t, spec, "Obsoletes: svn")
require.Contains(t, spec, "Recommends: git")
// Reproducible-rebuild pragmas and the tarball-extracting %install.
require.Contains(t, spec, "%global debug_package %{nil}")
require.Contains(t, spec, "AutoReqProv: no")
require.Contains(t, spec, "Source0: foo-1.0.0.tar.gz")
require.Contains(t, spec, "tar -C %{buildroot} -xf %{SOURCE0}")
// %files entries with directives.
require.Contains(t, spec, "/usr/bin/fake")
require.Contains(t, spec, "%config %attr")
require.Contains(t, spec, "/etc/fake/fake.conf")
require.Contains(t, spec, "%dir %attr")
// Scriptlets are inlined verbatim.
require.Contains(t, spec, "%pre\n")
require.Contains(t, spec, "%post\n")
require.Contains(t, spec, "%pretrans\n")
require.Contains(t, spec, "%posttrans\n")
require.Contains(t, spec, "%verifyscript\n")
require.Contains(t, spec, `echo "Preinstall"`)
}
func TestSRPMGenerateSpecFileDirectives(t *testing.T) {
info := nfpm.WithDefaults(&nfpm.Info{
Name: "spectest",
Arch: "amd64",
Version: "2.0.0",
Description: "spec directive coverage",
Maintainer: "maintainer",
Overridables: nfpm.Overridables{
Contents: []*files.Content{
{Source: "../testdata/fake", Destination: "/usr/bin/fake"},
{Source: "../testdata/whatever.conf", Destination: "/etc/cfg.conf", Type: files.TypeConfigNoReplace},
{Source: "../testdata/fake", Destination: "/usr/share/doc/readme", Type: files.TypeRPMDoc},
{Destination: "/var/log/ghost.log", Type: files.TypeRPMGhost},
{Source: "/usr/bin/fake", Destination: "/usr/bin/fakelink", Type: files.TypeSymlink},
{Destination: "/var/lib/spectest", Type: files.TypeDir},
},
},
})
info = setDefaults(info)
require.NoError(t, nfpm.PrepareForPackager(info, "rpm"))
spec, err := generateSpec(info, "spectest-2.0.0.tar.gz")
require.NoError(t, err)
require.Contains(t, spec, "%config(noreplace) %attr")
require.Contains(t, spec, "/etc/cfg.conf")
require.Contains(t, spec, "%doc %attr")
require.Contains(t, spec, "%ghost %attr")
require.Contains(t, spec, "/var/log/ghost.log")
require.Contains(t, spec, "%dir %attr")
require.Contains(t, spec, "/var/lib/spectest")
require.Contains(t, spec, `%attr(-, root, root) "/usr/bin/fakelink"`)
}
func TestSRPMGenerateSpecLang(t *testing.T) {
info := nfpm.WithDefaults(&nfpm.Info{
Name: "langtest",
Arch: "amd64",
Version: "1.0.0",
Description: "lang directive coverage",
Maintainer: "maintainer",
Overridables: nfpm.Overridables{
Contents: []*files.Content{
{
Source: "../testdata/whatever.conf",
Destination: "/usr/share/locale/en/LC_MESSAGES/langtest.mo",
FileInfo: &files.ContentFileInfo{Lang: "en"},
},
{
Source: "../testdata/whatever.conf",
Destination: "/etc/langtest.conf",
Type: files.TypeConfig,
FileInfo: &files.ContentFileInfo{Lang: "fr"},
},
},
},
})
info = setDefaults(info)
require.NoError(t, nfpm.PrepareForPackager(info, "rpm"))
spec, err := generateSpec(info, "langtest-1.0.0.tar.gz")
require.NoError(t, err)
require.Contains(t, spec, "%lang(en) %attr")
require.Contains(t, spec, "/usr/share/locale/en/LC_MESSAGES/langtest.mo")
require.Contains(t, spec, "%lang(fr) %config %attr")
require.Contains(t, spec, "/etc/langtest.conf")
}
// TestSRPMGenerateSpecEscapesPercent guards against user content being
// reinterpreted by rpmbuild on rebuild: every percent sign in scriptlets,
// free-text fields and file paths must be doubled so it stays literal.
func TestSRPMGenerateSpecEscapesPercent(t *testing.T) {
dir := t.TempDir()
script := filepath.Join(dir, "postinstall.sh")
// ${HOST%%.*} is the common longest-suffix-strip idiom; %{_libdir} and
// %(id -u) are macro/command forms. All must survive a rebuild verbatim.
require.NoError(t, os.WriteFile(script,
[]byte("#!/bin/sh\nshort=${HOST%%.*}\necho %{_libdir} %(id -u)\n"), 0o755))
info := nfpm.WithDefaults(&nfpm.Info{
Name: "esc",
Arch: "amd64",
Version: "1.0.0",
Description: "desc with %{_libdir} and %(echo hi)",
Maintainer: "maintainer",
Overridables: nfpm.Overridables{
Scripts: nfpm.Scripts{PostInstall: script},
RPM: nfpm.RPM{Summary: "summary 50% off"},
Contents: []*files.Content{
{Source: "../testdata/whatever.conf", Destination: "/etc/foo%bar.conf"},
},
},
})
info = setDefaults(info)
require.NoError(t, nfpm.PrepareForPackager(info, "rpm"))
spec, err := generateSpec(info, "esc-1.0.0.tar.gz")
require.NoError(t, err)
// Percent signs in user content are doubled; the bare ${HOST%.*} would
// silently strip the shortest suffix instead of the longest on rebuild.
require.Contains(t, spec, "short=${HOST%%%%.*}")
require.Contains(t, spec, "echo %%{_libdir} %%(id -u)")
require.Contains(t, spec, "Summary: summary 50%% off")
require.Contains(t, spec, "%description\ndesc with %%{_libdir} and %%(echo hi)")
require.Contains(t, spec, `/etc/foo%%bar.conf`)
require.NotContains(t, spec, "short=${HOST%.*}")
// Directives nfpm emits itself stay single-percent and functional.
require.Contains(t, spec, "%global debug_package %{nil}")
require.Contains(t, spec, "tar -C %{buildroot} -xf %{SOURCE0}")
}