diff --git a/acceptance_test.go b/acceptance_test.go index f6a9d02..0b21515 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -202,6 +202,7 @@ func TestRPMSpecific(t *testing.T) { "release", "directories", "verify", + "postrequires", } for _, name := range testNames { for _, arch := range formatArchs[format] { diff --git a/nfpm.go b/nfpm.go index b20ece0..e2daeec 100644 --- a/nfpm.go +++ b/nfpm.go @@ -220,6 +220,7 @@ func (c *Config) expandEnvVars() { c.Overrides[or].Recommends = c.expandEnvVarsStringSlice(c.Overrides[or].Recommends) c.Overrides[or].Provides = c.expandEnvVarsStringSlice(c.Overrides[or].Provides) c.Overrides[or].Suggests = c.expandEnvVarsStringSlice(c.Overrides[or].Suggests) + c.Overrides[or].RPM.Requires.Post = c.expandEnvVarsStringSlice(c.Overrides[or].RPM.Requires.Post) c.Overrides[or].Contents = c.expandEnvVarsContents(c.Overrides[or].Contents) } c.Conflicts = c.expandEnvVarsStringSlice(c.Conflicts) @@ -228,6 +229,7 @@ func (c *Config) expandEnvVars() { c.Recommends = c.expandEnvVarsStringSlice(c.Recommends) c.Provides = c.expandEnvVarsStringSlice(c.Provides) c.Suggests = c.expandEnvVarsStringSlice(c.Suggests) + c.RPM.Requires.Post = c.expandEnvVarsStringSlice(c.RPM.Requires.Post) c.Contents = c.expandEnvVarsContents(c.Contents) // Basic metadata fields @@ -392,6 +394,7 @@ type RPM struct { Arch string `yaml:"arch,omitempty" json:"arch,omitempty" jsonschema:"title=architecture in rpm nomenclature"` BuildHost string `yaml:"buildhost,omitempty" json:"buildhost,omitempty" jsonschema:"title=host name of the build environment, default=os.Hostname()"` Scripts RPMScripts `yaml:"scripts,omitempty" json:"scripts,omitempty" jsonschema:"title=rpm-specific scripts"` + Requires RPMRequires `yaml:"requires,omitempty" json:"requires,omitempty" jsonschema:"title=rpm-specific requires"` Group string `yaml:"group,omitempty" json:"group,omitempty" jsonschema:"title=package group,example=Unspecified"` Summary string `yaml:"summary,omitempty" json:"summary,omitempty" jsonschema:"title=package summary"` Compression string `yaml:"compression,omitempty" json:"compression,omitempty" jsonschema:"title=compression algorithm to be used,enum=gzip,enum=lzma,enum=xz,enum=zstd,default=gzip:-1"` @@ -407,6 +410,11 @@ type RPMScripts struct { Verify string `yaml:"verify,omitempty" json:"verify,omitempty" jsonschema:"title=verify script"` } +// RPMRequires represents qualified RPM Requires dependencies. +type RPMRequires struct { + Post []string `yaml:"post,omitempty" json:"post,omitempty" jsonschema:"title=post requires directive,example=nfpm"` +} + type PackageSignature struct { // PGP secret key, can be ASCII-armored KeyFile string `yaml:"key_file,omitempty" json:"key_file,omitempty" jsonschema:"title=key file,example=key.gpg"` diff --git a/rpm/rpm.go b/rpm/rpm.go index f24fa81..3c2131f 100644 --- a/rpm/rpm.go +++ b/rpm/rpm.go @@ -30,6 +30,9 @@ const ( tagChangelogText = 1082 // https://github.com/rpm-software-management/rpm/blob/master/include/rpm/rpmtag.h#L183 tagSourcePackage = 1106 + // RPMSENSE_SCRIPT_POST marks a dependency as Requires(post). + // https://github.com/rpm-software-management/rpm/blob/master/include/rpm/rpmds.h + rpmSenseScriptPost = 1 << 10 // Symbolic link tagLink = 0o120000 @@ -248,6 +251,9 @@ func buildRPMMeta(info *nfpm.Info) (*rpmpack.RPMMetaData, error) { if depends, err = toRelation(info.Depends); err != nil { return nil, err } + if err = addPostRequires(&depends, info.RPM.Requires.Post); err != nil { + return nil, err + } if recommends, err = toRelation(info.Recommends); err != nil { return nil, err } @@ -328,6 +334,19 @@ func toRelation(items []string) (rpmpack.Relations, error) { return relations, nil } +func addPostRequires(relations *rpmpack.Relations, items []string) error { + for idx := range items { + relation, err := rpmpack.NewRelation(items[idx]) + if err != nil { + return err + } + relation.Sense |= rpmSenseScriptPost + *relations = append(*relations, relation) + } + + return nil +} + func addScriptFiles(info *nfpm.Info, rpm *rpmpack.RPM) error { if info.RPM.Scripts.PreTrans != "" { data, err := os.ReadFile(info.RPM.Scripts.PreTrans) diff --git a/rpm/rpm_test.go b/rpm/rpm_test.go index a4d32df..0fda26d 100644 --- a/rpm/rpm_test.go +++ b/rpm/rpm_test.go @@ -22,6 +22,12 @@ import ( "github.com/stretchr/testify/require" ) +const ( + tagRequireFlags = 1048 + tagRequireName = 1049 + tagRequireVersion = 1050 +) + func exampleInfo() *nfpm.Info { return setDefaults(nfpm.WithDefaults(&nfpm.Info{ Name: "foo", @@ -154,6 +160,50 @@ func TestRPM(t *testing.T) { require.Equal(t, "Foo does things", description) } +func TestRPMPostRequires(t *testing.T) { + info := exampleInfo() + info.RPM.Requires.Post = []string{"systemd", "coreutils >= 9.0"} + + var buf bytes.Buffer + err := DefaultRPM.Package(info, &buf) + require.NoError(t, err) + + rpm, err := rpmutils.ReadRpm(&buf) + require.NoError(t, err) + + namesRaw, err := rpm.Header.Get(tagRequireName) + require.NoError(t, err) + names, ok := namesRaw.([]string) + require.True(t, ok) + versionsRaw, err := rpm.Header.Get(tagRequireVersion) + require.NoError(t, err) + versions, ok := versionsRaw.([]string) + require.True(t, ok) + flagsRaw, err := rpm.Header.Get(tagRequireFlags) + require.NoError(t, err) + flags, ok := flagsRaw.([]uint32) + require.True(t, ok) + + requireRPMRequire(t, names, versions, flags, "systemd", "", rpmSenseScriptPost) + requireRPMRequire(t, names, versions, flags, "coreutils", "9.0", rpmSenseScriptPost|4|8) +} + +func requireRPMRequire( + t *testing.T, + names, versions []string, + flags []uint32, + name, version string, + flag uint32, +) { + t.Helper() + for idx := range names { + if names[idx] == name && versions[idx] == version && flags[idx] == flag { + return + } + } + require.Failf(t, "RPM require not found", "name=%s version=%s flag=%d", name, version, flag) +} + func TestRPMRiscv64(t *testing.T) { f, err := os.CreateTemp(t.TempDir(), "test-riscv.rpm") require.NoError(t, err) diff --git a/testdata/acceptance/rpm.dockerfile b/testdata/acceptance/rpm.dockerfile index 29a300c..790c746 100644 --- a/testdata/acceptance/rpm.dockerfile +++ b/testdata/acceptance/rpm.dockerfile @@ -226,3 +226,9 @@ FROM min AS verify RUN rpm -V foo RUN rm /tmp/postinstall-proof RUN ! rpm -V foo + +# ---- postrequires test ---- +FROM min AS postrequires +RUN rpm -qp --qf '[%{REQUIRENAME} %{REQUIREFLAGS} %{REQUIREVERSION}\n]' /tmp/foo.rpm +RUN rpm -qp --qf '[%{REQUIRENAME} %{REQUIREFLAGS} %{REQUIREVERSION}\n]' /tmp/foo.rpm | grep -E '^bash 1024 ?$' +RUN rpm -qp --qf '[%{REQUIRENAME} %{REQUIREFLAGS} %{REQUIREVERSION}\n]' /tmp/foo.rpm | grep -E '^coreutils 1036 9\.0$' diff --git a/testdata/acceptance/rpm.postrequires.yaml b/testdata/acceptance/rpm.postrequires.yaml new file mode 100644 index 0000000..80d2da2 --- /dev/null +++ b/testdata/acceptance/rpm.postrequires.yaml @@ -0,0 +1,20 @@ +name: "foo" +arch: "${BUILD_ARCH}" +platform: "linux" +version: "v1.2.3" +maintainer: "Foo Bar" +release: "4" +description: | + Foo bar + Multiple lines +vendor: "foobar" +homepage: "https://foobar.org" +license: "MIT" +contents: +- src: ./testdata/fake + dst: /usr/bin/fake +rpm: + requires: + post: + - bash + - coreutils >= 9.0 diff --git a/www/content/docs/configuration.md b/www/content/docs/configuration.md index 59b1f9d..a5f9556 100644 --- a/www/content/docs/configuration.md +++ b/www/content/docs/configuration.md @@ -340,6 +340,12 @@ rpm: # The verify script runs when verifying packages using `rpm -V`. verify: ./scripts/verify.sh + # RPM specific qualified Requires dependencies. + requires: + # Adds `Requires(post): systemd`. + post: + - systemd + # The package group. This option is deprecated by most distros # but required by old distros like CentOS 5 / EL 5 and earlier. group: Unspecified diff --git a/www/static/schema.json b/www/static/schema.json index fd2e96f..1200c06 100644 --- a/www/static/schema.json +++ b/www/static/schema.json @@ -974,6 +974,10 @@ "$ref": "#/$defs/RPMScripts", "title": "rpm-specific scripts" }, + "requires": { + "$ref": "#/$defs/RPMRequires", + "title": "rpm-specific requires" + }, "group": { "type": "string", "title": "package group", @@ -1015,6 +1019,22 @@ "additionalProperties": false, "type": "object" }, + "RPMRequires": { + "properties": { + "post": { + "items": { + "type": "string", + "examples": [ + "nfpm" + ] + }, + "type": "array", + "title": "post requires directive" + } + }, + "additionalProperties": false, + "type": "object" + }, "RPMScripts": { "properties": { "pretrans": {