Repository: goreleaser/nfpm
Pull request #548 (2022-09-23 17:23:25)

feat: allow env expansion in conflicts, suggests, recommends, depends…
* Add env expansion in conflicts, suggests, recommends, depends, provides, and replaces. Ensuring empty env vars get stripped to not cause issues.
* Added test for boolean dependencies for rpm, deb, and apk.

codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/nfpm/pull/548?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) Report
> Merging [#548](https://codecov.io/gh/goreleaser/nfpm/pull/548?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (eeea9ba) into [main](https://codecov.io/gh/goreleaser/nfpm/commit/e78d8b58249dda6efb1bc47127217e71fd98629a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (e78d8b5) will **increase** coverage by `0.28%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##             main     #548      +/-   ##
==========================================
+ Coverage   65.98%   66.26%   +0.28%     
==========================================
  Files          16       16              
  Lines        1993     2010      +17     
==========================================
+ Hits         1315     1332      +17     
  Misses        533      533              
  Partials      145      145              
```


| [Impacted Files](https://codecov.io/gh/goreleaser/nfpm/pull/548?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) | Coverage Δ | |
|---|---|---|
| [nfpm.go](https://codecov.io/gh/goreleaser/nfpm/pull/548/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-bmZwbS5nbw==) | `88.00% <100.00%> (+1.11%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)


Patch:
diff --git a/go.mod b/go.mod
index 3184531..df49047 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,6 @@ require (
 	github.com/ProtonMail/gopenpgp/v2 v2.2.2
 	github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
 	github.com/caarlos0/go-rpmutils v0.2.1-0.20211112020245-2cd62ff89b11
-	github.com/google/rpmpack v0.0.0-20220314092521-38642b5e571e
 	github.com/goreleaser/chglog v0.2.2
 	github.com/goreleaser/fileglob v1.3.0
 	github.com/imdario/mergo v0.3.13
@@ -24,6 +23,13 @@ require (
 	gopkg.in/yaml.v3 v3.0.1
 )
 
+require (
+	github.com/cavaliergopher/cpio v1.0.1
+	github.com/google/go-cmp v0.5.8
+	github.com/klauspost/compress v1.15.1
+	github.com/pkg/errors v0.9.1
+)
+
 require (
 	github.com/DataDog/zstd v1.4.5 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
@@ -32,7 +38,6 @@ require (
 	github.com/Microsoft/go-winio v0.5.1 // indirect
 	github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f // indirect
 	github.com/acomagu/bufpipe v1.0.3 // indirect
-	github.com/cavaliergopher/cpio v1.0.1 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/emirpasic/gods v1.12.0 // indirect
@@ -46,13 +51,11 @@ require (
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
 	github.com/kevinburke/ssh_config v1.1.0 // indirect
-	github.com/klauspost/compress v1.13.6 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/muesli/mango v0.1.0 // indirect
 	github.com/muesli/mango-pflag v0.1.0 // indirect
-	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/sergi/go-diff v1.2.0 // indirect
diff --git a/go.sum b/go.sum
index 809aac9..d8e92b9 100644
--- a/go.sum
+++ b/go.sum
@@ -61,10 +61,8 @@ github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
-github.com/google/rpmpack v0.0.0-20220314092521-38642b5e571e h1:6Jn9JtfCn20uycra92LxTkq5yfBKNSFlRJPBk8/Cxhg=
-github.com/google/rpmpack v0.0.0-20220314092521-38642b5e571e/go.mod h1:83rLnx5vhPyN/mDzBYJWtiPf+9xnSVQynTpqZWe7OnY=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
@@ -92,8 +90,8 @@ github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT
 github.com/kevinburke/ssh_config v1.1.0 h1:pH/t1WS9NzT8go394IqZeJTMHVm6Cr6ZJ6AQ+mdNo/o=
 github.com/kevinburke/ssh_config v1.1.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
-github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
-github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A=
+github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
 github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
diff --git a/internal/rpmpack/LICENSE b/internal/rpmpack/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/internal/rpmpack/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/internal/rpmpack/dir.go b/internal/rpmpack/dir.go
new file mode 100644
index 0000000..031cad1
--- /dev/null
+++ b/internal/rpmpack/dir.go
@@ -0,0 +1,40 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package rpmpack
+
+// dirIndex holds the index from files to directory names.
+type dirIndex struct {
+	m map[string]uint32
+	l []string
+}
+
+func newDirIndex() *dirIndex {
+	return &dirIndex{m: make(map[string]uint32)}
+}
+
+func (d *dirIndex) Get(value string) uint32 {
+	if idx, ok := d.m[value]; ok {
+		return idx
+	}
+	newIdx := uint32(len(d.l))
+	d.l = append(d.l, value)
+
+	d.m[value] = newIdx
+	return newIdx
+}
+
+func (d *dirIndex) AllDirs() []string {
+	return d.l
+}
diff --git a/internal/rpmpack/file_types.go b/internal/rpmpack/file_types.go
new file mode 100644
index 0000000..720f733
--- /dev/null
+++ b/internal/rpmpack/file_types.go
@@ -0,0 +1,47 @@
+package rpmpack
+
+// FileType is the type of a file inside a RPM package.
+type FileType int32
+
+// https://refspecs.linuxbase.org/LSB_3.1.1/LSB-Core-generic/LSB-Core-generic/pkgformat.html#AEN27560
+// The RPMFile.Type tag value shall identify various characteristics of the file in the payload that it describes.
+// It shall be an INT32 value consisting of either the value GenericFile (0) or
+// the bitwise inclusive or of one or more of the following values. Some of these combinations may make no sense
+const (
+	// GenericFile is just a basic file in an RPM
+	GenericFile FileType = 1 << iota >> 1
+	// ConfigFile is a configuration file, and an existing file should be saved during a
+	// package upgrade operation and not removed during a package removal operation.
+	ConfigFile
+	// DocFile is a file that contains documentation.
+	DocFile
+	// DoNotUseFile is reserved for future use; conforming packages may not use this flag.
+	DoNotUseFile
+	// MissingOkFile need not exist on the installed system.
+	MissingOkFile
+	// NoReplaceFile similar to the ConfigFile, this flag indicates that during an upgrade operation
+	// the original file on the system should not be altered.
+	NoReplaceFile
+	// SpecFile is the package specification file.
+	SpecFile
+	// GhostFile is not actually included in the payload, but should still be considered as a part of the package.
+	// For example, a log file generated by the application at run time.
+	GhostFile
+	// LicenceFile contains the license conditions.
+	LicenceFile
+	// ReadmeFile contains high level notes about the package.
+	ReadmeFile
+	// ExcludeFile is not a part of the package, and should not be installed.
+	ExcludeFile
+)
+
+// RPMFile contains a particular file's entry and data.
+type RPMFile struct {
+	Name  string
+	Body  []byte
+	Mode  uint
+	Owner string
+	Group string
+	MTime uint32
+	Type  FileType
+}
diff --git a/internal/rpmpack/header.go b/internal/rpmpack/header.go
new file mode 100644
index 0000000..b223672
--- /dev/null
+++ b/internal/rpmpack/header.go
@@ -0,0 +1,211 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package rpmpack
+
+import (
+	"bytes"
+	"encoding/binary"
+	"fmt"
+	"sort"
+
+	"github.com/pkg/errors"
+)
+
+const (
+	signatures = 0x3e
+	immutable  = 0x3f
+
+	typeInt16       = 0x03
+	typeInt32       = 0x04
+	typeString      = 0x06
+	typeBinary      = 0x07
+	typeStringArray = 0x08
+)
+
+// Only integer types are aligned. This is not just an optimization - some versions
+// of rpm fail when integers are not aligned. Other versions fail when non-integers are aligned.
+var boundaries = map[int]int{
+	typeInt16: 2,
+	typeInt32: 4,
+}
+
+type IndexEntry struct {
+	rpmtype, count int
+	data           []byte
+}
+
+func (e IndexEntry) indexBytes(tag, contentOffset int) []byte {
+	b := &bytes.Buffer{}
+	if err := binary.Write(b, binary.BigEndian, []int32{int32(tag), int32(e.rpmtype), int32(contentOffset), int32(e.count)}); err != nil {
+		// binary.Write can fail if the underlying Write fails, or the types are invalid.
+		// bytes.Buffer's write never error out, it can only panic with OOM.
+		panic(err)
+	}
+	return b.Bytes()
+}
+
+func intEntry(rpmtype, size int, value interface{}) IndexEntry {
+	b := &bytes.Buffer{}
+	if err := binary.Write(b, binary.BigEndian, value); err != nil {
+		// binary.Write can fail if the underlying Write fails, or the types are invalid.
+		// bytes.Buffer's write never error out, it can only panic with OOM.
+		panic(err)
+	}
+	return IndexEntry{rpmtype, size, b.Bytes()}
+}
+
+func EntryInt16(value []int16) IndexEntry {
+	return intEntry(typeInt16, len(value), value)
+}
+
+func EntryUint16(value []uint16) IndexEntry {
+	return intEntry(typeInt16, len(value), value)
+}
+
+func EntryInt32(value []int32) IndexEntry {
+	return intEntry(typeInt32, len(value), value)
+}
+
+func EntryUint32(value []uint32) IndexEntry {
+	return intEntry(typeInt32, len(value), value)
+}
+
+func EntryString(value string) IndexEntry {
+	return IndexEntry{typeString, 1, append([]byte(value), byte(0o0))}
+}
+
+func EntryBytes(value []byte) IndexEntry {
+	return IndexEntry{typeBinary, len(value), value}
+}
+
+func EntryStringSlice(value []string) IndexEntry {
+	b := [][]byte{}
+	for _, v := range value {
+		b = append(b, []byte(v))
+	}
+	bb := append(bytes.Join(b, []byte{0o0}), byte(0o0))
+	return IndexEntry{typeStringArray, len(value), bb}
+}
+
+type index struct {
+	entries map[int]IndexEntry
+	h       int
+}
+
+func newIndex(h int) *index {
+	return &index{entries: make(map[int]IndexEntry), h: h}
+}
+
+func (i *index) Add(tag int, e IndexEntry) {
+	i.entries[tag] = e
+}
+
+func (i *index) AddEntries(m map[int]IndexEntry) {
+	for t, e := range m {
+		i.Add(t, e)
+	}
+}
+
+func (i *index) sortedTags() []int {
+	t := []int{}
+	for k := range i.entries {
+		t = append(t, k)
+	}
+	sort.Ints(t)
+	return t
+}
+
+func pad(w *bytes.Buffer, rpmtype, offset int) {
+	// We need to align integer entries...
+	if b, ok := boundaries[rpmtype]; ok && offset%b != 0 {
+		if _, err := w.Write(make([]byte, b-offset%b)); err != nil {
+			// binary.Write can fail if the underlying Write fails, or the types are invalid.
+			// bytes.Buffer's write never error out, it can only panic with OOM.
+			panic(err)
+		}
+	}
+}
+
+// Bytes returns the bytes of the index.
+func (i *index) Bytes() ([]byte, error) {
+	w := &bytes.Buffer{}
+	// Even the header has three parts: The lead, the index entries, and the entries.
+	// Because of alignment, we can only tell the actual size and offset after writing
+	// the entries.
+	entryData := &bytes.Buffer{}
+	tags := i.sortedTags()
+	offsets := make([]int, len(tags))
+	for ii, tag := range tags {
+		e := i.entries[tag]
+		pad(entryData, e.rpmtype, entryData.Len())
+		offsets[ii] = entryData.Len()
+		entryData.Write(e.data)
+	}
+	entryData.Write(i.eigenHeader().data)
+
+	// 4 magic and 4 reserved
+	w.Write([]byte{0x8e, 0xad, 0xe8, 0x01, 0, 0, 0, 0})
+	// 4 count and 4 size
+	// We add the pseudo-entry "eigenHeader" to count.
+	if err := binary.Write(w, binary.BigEndian, []int32{int32(len(i.entries)) + 1, int32(entryData.Len())}); err != nil {
+		return nil, errors.Wrap(err, "failed to write eigenHeader")
+	}
+	// Write the eigenHeader index entry
+	w.Write(i.eigenHeader().indexBytes(i.h, entryData.Len()-0x10))
+	// Write all of the other index entries
+	for ii, tag := range tags {
+		e := i.entries[tag]
+		w.Write(e.indexBytes(tag, offsets[ii]))
+	}
+	w.Write(entryData.Bytes())
+	return w.Bytes(), nil
+}
+
+// the eigenHeader is a weird entry. Its index entry is sorted first, but its content
+// is last. The content is a 16 byte index entry, which is almost the same as the index
+// entry except for the offset. The offset here is ... minus the length of the index entry region.
+// Which is always 0x10 * number of entries.
+// I kid you not.
+func (i *index) eigenHeader() IndexEntry {
+	b := &bytes.Buffer{}
+	if err := binary.Write(b, binary.BigEndian, []int32{int32(i.h), int32(typeBinary), -int32(0x10 * (len(i.entries) + 1)), int32(0x10)}); err != nil {
+		// binary.Write can fail if the underlying Write fails, or the types are invalid.
+		// bytes.Buffer's write never error out, it can only panic with OOM.
+		panic(err)
+	}
+
+	return EntryBytes(b.Bytes())
+}
+
+func lead(name, fullVersion string) []byte {
+	// RPM format = 0xedabeedb
+	// version 3.0 = 0x0300
+	// type binary = 0x0000
+	// machine archnum (i386?) = 0x0001
+	// name ( 66 bytes, with null termination)
+	// osnum (linux?) = 0x0001
+	// sig type (header-style) = 0x0005
+	// reserved 16 bytes of 0x00
+	n := []byte(fmt.Sprintf("%s-%s", name, fullVersion))
+	if len(n) > 65 {
+		n = n[:65]
+	}
+	n = append(n, make([]byte, 66-len(n))...)
+	b := []byte{0xed, 0xab, 0xee, 0xdb, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01}
+	b = append(b, n...)
+	b = append(b, []byte{0x00, 0x01, 0x00, 0x05}...)
+	b = append(b, make([]byte, 16)...)
+	return b
+}
diff --git a/internal/rpmpack/rpm.go b/internal/rpmpack/rpm.go
new file mode 100644
index 0000000..2c243e4
--- /dev/null
+++ b/internal/rpmpack/rpm.go
@@ -0,0 +1,550 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package rpmpack packs files to rpm files.
+// It is designed to be simple to use and deploy, not requiring any filesystem access
+// to create rpm files.
+package rpmpack
+
+import (
+	"bytes"
+	"crypto/sha256"
+	"fmt"
+	"io"
+	"path"
+	"sort"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/cavaliergopher/cpio"
+	"github.com/klauspost/compress/zstd"
+	gzip "github.com/klauspost/pgzip"
+	"github.com/pkg/errors"
+	"github.com/ulikunitz/xz"
+	"github.com/ulikunitz/xz/lzma"
+)
+
+var (
+	// ErrWriteAfterClose is returned when a user calls Write() on a closed rpm.
+	ErrWriteAfterClose = errors.New("rpm write after close")
+	// ErrWrongFileOrder is returned when files are not sorted by name.
+	ErrWrongFileOrder = errors.New("wrong file addition order")
+)
+
+// RPMMetaData contains meta info about the whole package.
+type RPMMetaData struct {
+	Name,
+	Summary,
+	Description,
+	Version,
+	Release,
+	Arch,
+	OS,
+	Vendor,
+	URL,
+	Packager,
+	Group,
+	Licence,
+	BuildHost,
+	Compressor string
+	Epoch     uint32
+	BuildTime time.Time
+	Provides,
+	Obsoletes,
+	Suggests,
+	Recommends,
+	Requires,
+	Conflicts Relations
+}
+
+// RPM holds the state of a particular rpm file. Please use NewRPM to instantiate it.
+type RPM struct {
+	RPMMetaData
+	di                *dirIndex
+	payload           *bytes.Buffer
+	payloadSize       uint
+	cpio              *cpio.Writer
+	basenames         []string
+	dirindexes        []uint32
+	filesizes         []uint32
+	filemodes         []uint16
+	fileowners        []string
+	filegroups        []string
+	filemtimes        []uint32
+	filedigests       []string
+	filelinktos       []string
+	fileflags         []uint32
+	closed            bool
+	compressedPayload io.WriteCloser
+	files             map[string]RPMFile
+	prein             string
+	postin            string
+	preun             string
+	postun            string
+	pretrans          string
+	posttrans         string
+	customTags        map[int]IndexEntry
+	customSigs        map[int]IndexEntry
+	pgpSigner         func([]byte) ([]byte, error)
+}
+
+// NewRPM creates and returns a new RPM struct.
+func NewRPM(m RPMMetaData) (*RPM, error) {
+	var err error
+
+	if m.OS == "" {
+		m.OS = "linux"
+	}
+
+	if m.Arch == "" {
+		m.Arch = "noarch"
+	}
+
+	p := &bytes.Buffer{}
+
+	z, compressorName, err := setupCompressor(m.Compressor, p)
+	if err != nil {
+		return nil, err
+	}
+
+	// only use compressor name for the rpm tag, not the level
+	m.Compressor = compressorName
+
+	rpm := &RPM{
+		RPMMetaData:       m,
+		di:                newDirIndex(),
+		payload:           p,
+		compressedPayload: z,
+		cpio:              cpio.NewWriter(z),
+		files:             make(map[string]RPMFile),
+		customTags:        make(map[int]IndexEntry),
+		customSigs:        make(map[int]IndexEntry),
+	}
+
+	// A package must provide itself...
+	rpm.Provides.addIfMissing(&Relation{
+		Name:    rpm.Name,
+		Version: rpm.FullVersion(),
+		Sense:   SenseEqual,
+	})
+
+	return rpm, nil
+}
+
+func setupCompressor(compressorSetting string, w io.Writer) (wc io.WriteCloser,
+	compressorType string, err error,
+) {
+	parts := strings.Split(compressorSetting, ":")
+	if len(parts) > 2 {
+		return nil, "", fmt.Errorf("malformed compressor setting: %s", compressorSetting)
+	}
+
+	compressorType = parts[0]
+	compressorLevel := ""
+	if len(parts) == 2 {
+		compressorLevel = parts[1]
+	}
+
+	switch compressorType {
+	case "":
+		compressorType = "gzip"
+		fallthrough
+	case "gzip":
+		level := 9
+
+		if compressorLevel != "" {
+			var err error
+
+			level, err = strconv.Atoi(compressorLevel)
+			if err != nil {
+				return nil, "", fmt.Errorf("parse gzip compressor level: %w", err)
+			}
+		}
+
+		wc, err = gzip.NewWriterLevel(w, level)
+	case "lzma":
+		if compressorLevel != "" {
+			return nil, "", fmt.Errorf("no compressor level supported for lzma: %s", compressorLevel)
+		}
+
+		wc, err = lzma.NewWriter(w)
+	case "xz":
+		if compressorLevel != "" {
+			return nil, "", fmt.Errorf("no compressor level supported for xz: %s", compressorLevel)
+		}
+
+		wc, err = xz.NewWriter(w)
+	case "zstd":
+		level := zstd.SpeedBetterCompression
+
+		if compressorLevel != "" {
+			var ok bool
+
+			if intLevel, err := strconv.Atoi(compressorLevel); err == nil {
+				level = zstd.EncoderLevelFromZstd(intLevel)
+			} else {
+				ok, level = zstd.EncoderLevelFromString(compressorLevel)
+				if !ok {
+					return nil, "", fmt.Errorf("invalid zstd compressor level: %s", compressorLevel)
+				}
+			}
+		}
+
+		wc, err = zstd.NewWriter(w, zstd.WithEncoderLevel(level))
+	default:
+		return nil, "", fmt.Errorf("unknown compressor type: %s", compressorType)
+	}
+
+	return wc, compressorType, err
+}
+
+// FullVersion properly combines version and release fields to a version string
+func (r *RPM) FullVersion() string {
+	if r.Release != "" {
+		return fmt.Sprintf("%s-%s", r.Version, r.Release)
+	}
+
+	return r.Version
+}
+
+// AllowListDirs removes all directories which are not explicitly allowlisted.
+func (r *RPM) AllowListDirs(allowList map[string]bool) {
+	for fn, ff := range r.files {
+		if ff.Mode&0o40000 == 0o40000 {
+			if !allowList[fn] {
+				delete(r.files, fn)
+			}
+		}
+	}
+}
+
+// Write closes the rpm and writes the whole rpm to an io.Writer
+func (r *RPM) Write(w io.Writer) error {
+	if r.closed {
+		return ErrWriteAfterClose
+	}
+	// Add all of the files, sorted alphabetically.
+	fnames := []string{}
+	for fn := range r.files {
+		fnames = append(fnames, fn)
+	}
+	sort.Strings(fnames)
+	for _, fn := range fnames {
+		if err := r.writeFile(r.files[fn]); err != nil {
+			return errors.Wrapf(err, "failed to write file %q", fn)
+		}
+	}
+	if err := r.cpio.Close(); err != nil {
+		return errors.Wrap(err, "failed to close cpio payload")
+	}
+	if err := r.compressedPayload.Close(); err != nil {
+		return errors.Wrap(err, "failed to close gzip payload")
+	}
+
+	if _, err := w.Write(lead(r.Name, r.FullVersion())); err != nil {
+		return errors.Wrap(err, "failed to write lead")
+	}
+	// Write the regular header.
+	h := newIndex(immutable)
+	r.writeGenIndexes(h)
+
+	// do not write file indexes if there are no files (meta package)
+	// doing so will result in an invalid package
+	if (len(r.files)) > 0 {
+		r.writeFileIndexes(h)
+	}
+
+	if err := r.writeRelationIndexes(h); err != nil {
+		return err
+	}
+	// CustomTags must be the last to be added, because they can overwrite values.
+	h.AddEntries(r.customTags)
+	hb, err := h.Bytes()
+	if err != nil {
+		return errors.Wrap(err, "failed to retrieve header")
+	}
+	// Write the signatures
+	s := newIndex(signatures)
+	if err := r.writeSignatures(s, hb); err != nil {
+		return errors.Wrap(err, "failed to create signatures")
+	}
+
+	s.AddEntries(r.customSigs)
+	sb, err := s.Bytes()
+	if err != nil {
+		return errors.Wrap(err, "failed to retrieve signatures header")
+	}
+
+	if _, err := w.Write(sb); err != nil {
+		return errors.Wrap(err, "failed to write signature bytes")
+	}
+	// Signatures are padded to 8-byte boundaries
+	if _, err := w.Write(make([]byte, (8-len(sb)%8)%8)); err != nil {
+		return errors.Wrap(err, "failed to write signature padding")
+	}
+	if _, err := w.Write(hb); err != nil {
+		return errors.Wrap(err, "failed to write header body")
+	}
+	_, err = w.Write(r.payload.Bytes())
+	return errors.Wrap(err, "failed to write payload")
+}
+
+// SetPGPSigner registers a function that will accept the header and payload as bytes,
+// and return a signature as bytes. The function should simulate what gpg does,
+// probably by using golang.org/x/crypto/openpgp or by forking a gpg process.
+func (r *RPM) SetPGPSigner(f func([]byte) ([]byte, error)) {
+	r.pgpSigner = f
+}
+
+// Only call this after the payload and header were written.
+func (r *RPM) writeSignatures(sigHeader *index, regHeader []byte) error {
+	sigHeader.Add(sigSize, EntryInt32([]int32{int32(r.payload.Len() + len(regHeader))}))
+	sigHeader.Add(sigSHA256, EntryString(fmt.Sprintf("%x", sha256.Sum256(regHeader))))
+	sigHeader.Add(sigPayloadSize, EntryInt32([]int32{int32(r.payloadSize)}))
+	if r.pgpSigner != nil {
+		// For sha 256 you need to sign the header and payload separately
+		header := append([]byte{}, regHeader...)
+		headerSig, err := r.pgpSigner(header)
+		if err != nil {
+			return errors.Wrap(err, "call to signer failed")
+		}
+		sigHeader.Add(sigRSA, EntryBytes(headerSig))
+
+		body := append(header, r.payload.Bytes()...)
+		bodySig, err := r.pgpSigner(body)
+		if err != nil {
+			return errors.Wrap(err, "call to signer failed")
+		}
+		sigHeader.Add(sigPGP, EntryBytes(bodySig))
+	}
+	return nil
+}
+
+func (r *RPM) writeRelationIndexes(h *index) error {
+	// add all relation categories
+	if err := r.Provides.AddToIndex(h, tagProvides, tagProvideVersion, tagProvideFlags); err != nil {
+		return errors.Wrap(err, "failed to add provides")
+	}
+	if err := r.Obsoletes.AddToIndex(h, tagObsoletes, tagObsoleteVersion, tagObsoleteFlags); err != nil {
+		return errors.Wrap(err, "failed to add obsoletes")
+	}
+	if err := r.Suggests.AddToIndex(h, tagSuggests, tagSuggestVersion, tagSuggestFlags); err != nil {
+		return errors.Wrap(err, "failed to add suggests")
+	}
+	if err := r.Recommends.AddToIndex(h, tagRecommends, tagRecommendVersion, tagRecommendFlags); err != nil {
+		return errors.Wrap(err, "failed to add recommends")
+	}
+	if err := r.Requires.AddToIndex(h, tagRequires, tagRequireVersion, tagRequireFlags); err != nil {
+		return errors.Wrap(err, "failed to add requires")
+	}
+	if err := r.Conflicts.AddToIndex(h, tagConflicts, tagConflictVersion, tagConflictFlags); err != nil {
+		return errors.Wrap(err, "failed to add conflicts")
+	}
+
+	return nil
+}
+
+// AddCustomTag adds or overwrites a tag value in the index.
+func (r *RPM) AddCustomTag(tag int, e IndexEntry) {
+	r.customTags[tag] = e
+}
+
+// AddCustomSig adds or overwrites a signature tag value.
+func (r *RPM) AddCustomSig(tag int, e IndexEntry) {
+	r.customSigs[tag] = e
+}
+
+func (r *RPM) writeGenIndexes(h *index) {
+	h.Add(tagHeaderI18NTable, EntryString("C"))
+	h.Add(tagSize, EntryInt32([]int32{int32(r.payloadSize)}))
+	h.Add(tagName, EntryString(r.Name))
+	h.Add(tagVersion, EntryString(r.Version))
+	h.Add(tagEpoch, EntryUint32([]uint32{r.Epoch}))
+	h.Add(tagSummary, EntryString(r.Summary))
+	h.Add(tagDescription, EntryString(r.Description))
+	h.Add(tagBuildHost, EntryString(r.BuildHost))
+	if !r.BuildTime.IsZero() {
+		// time.Time zero value is confusing, avoid if not supplied
+		// see https://github.com/google/rpmpack/issues/43
+		h.Add(tagBuildTime, EntryInt32([]int32{int32(r.BuildTime.Unix())}))
+	}
+	h.Add(tagRelease, EntryString(r.Release))
+	h.Add(tagPayloadFormat, EntryString("cpio"))
+	h.Add(tagPayloadCompressor, EntryString(r.Compressor))
+	h.Add(tagPayloadFlags, EntryString("9"))
+	h.Add(tagArch, EntryString(r.Arch))
+	h.Add(tagOS, EntryString(r.OS))
+	h.Add(tagVendor, EntryString(r.Vendor))
+	h.Add(tagLicence, EntryString(r.Licence))
+	h.Add(tagPackager, EntryString(r.Packager))
+	h.Add(tagGroup, EntryString(r.Group))
+	h.Add(tagURL, EntryString(r.URL))
+	h.Add(tagPayloadDigest, EntryStringSlice([]string{fmt.Sprintf("%x", sha256.Sum256(r.payload.Bytes()))}))
+	h.Add(tagPayloadDigestAlgo, EntryInt32([]int32{hashAlgoSHA256}))
+
+	// rpm utilities look for the sourcerpm tag to deduce if this is not a source rpm (if it has a sourcerpm,
+	// it is NOT a source rpm).
+	h.Add(tagSourceRPM, EntryString(fmt.Sprintf("%s-%s.src.rpm", r.Name, r.FullVersion())))
+	if r.pretrans != "" {
+		h.Add(tagPretrans, EntryString(r.pretrans))
+		h.Add(tagPretransProg, EntryString("/bin/sh"))
+	}
+	if r.prein != "" {
+		h.Add(tagPrein, EntryString(r.prein))
+		h.Add(tagPreinProg, EntryString("/bin/sh"))
+	}
+	if r.postin != "" {
+		h.Add(tagPostin, EntryString(r.postin))
+		h.Add(tagPostinProg, EntryString("/bin/sh"))
+	}
+	if r.preun != "" {
+		h.Add(tagPreun, EntryString(r.preun))
+		h.Add(tagPreunProg, EntryString("/bin/sh"))
+	}
+	if r.postun != "" {
+		h.Add(tagPostun, EntryString(r.postun))
+		h.Add(tagPostunProg, EntryString("/bin/sh"))
+	}
+	if r.posttrans != "" {
+		h.Add(tagPosttrans, EntryString(r.posttrans))
+		h.Add(tagPosttransProg, EntryString("/bin/sh"))
+	}
+}
+
+// WriteFileIndexes writes file related index headers to the header
+func (r *RPM) writeFileIndexes(h *index) {
+	h.Add(tagBasenames, EntryStringSlice(r.basenames))
+	h.Add(tagDirindexes, EntryUint32(r.dirindexes))
+	h.Add(tagDirnames, EntryStringSlice(r.di.AllDirs()))
+	h.Add(tagFileSizes, EntryUint32(r.filesizes))
+	h.Add(tagFileModes, EntryUint16(r.filemodes))
+	h.Add(tagFileUserName, EntryStringSlice(r.fileowners))
+	h.Add(tagFileGroupName, EntryStringSlice(r.filegroups))
+	h.Add(tagFileMTimes, EntryUint32(r.filemtimes))
+	h.Add(tagFileDigests, EntryStringSlice(r.filedigests))
+	h.Add(tagFileLinkTos, EntryStringSlice(r.filelinktos))
+	h.Add(tagFileFlags, EntryUint32(r.fileflags))
+
+	inodes := make([]int32, len(r.dirindexes))
+	digestAlgo := make([]int32, len(r.dirindexes))
+	verifyFlags := make([]int32, len(r.dirindexes))
+	fileRDevs := make([]int16, len(r.dirindexes))
+	fileLangs := make([]string, len(r.dirindexes))
+
+	for ii := range inodes {
+		// is inodes just a range from 1..len(dirindexes)? maybe different with hard links
+		inodes[ii] = int32(ii + 1)
+		digestAlgo[ii] = hashAlgoSHA256
+		// With regular files, it seems like we can always enable all of the verify flags
+		verifyFlags[ii] = int32(-1)
+		fileRDevs[ii] = int16(1)
+	}
+	h.Add(tagFileINodes, EntryInt32(inodes))
+	h.Add(tagFileDigestAlgo, EntryInt32(digestAlgo))
+	h.Add(tagFileVerifyFlags, EntryInt32(verifyFlags))
+	h.Add(tagFileRDevs, EntryInt16(fileRDevs))
+	h.Add(tagFileLangs, EntryStringSlice(fileLangs))
+}
+
+// AddPretrans adds a pretrans scriptlet
+func (r *RPM) AddPretrans(s string) {
+	r.pretrans = s
+}
+
+// AddPrein adds a prein scriptlet
+func (r *RPM) AddPrein(s string) {
+	r.prein = s
+}
+
+// AddPostin adds a postin scriptlet
+func (r *RPM) AddPostin(s string) {
+	r.postin = s
+}
+
+// AddPreun adds a preun scriptlet
+func (r *RPM) AddPreun(s string) {
+	r.preun = s
+}
+
+// AddPostun adds a postun scriptlet
+func (r *RPM) AddPostun(s string) {
+	r.postun = s
+}
+
+// AddPosttrans adds a posttrans scriptlet
+func (r *RPM) AddPosttrans(s string) {
+	r.posttrans = s
+}
+
+// AddFile adds an RPMFile to an existing rpm.
+func (r *RPM) AddFile(f RPMFile) {
+	if f.Name == "/" { // rpm does not allow the root dir to be included.
+		return
+	}
+	r.files[f.Name] = f
+}
+
+// writeFile writes the file to the indexes and cpio.
+func (r *RPM) writeFile(f RPMFile) error {
+	dir, file := path.Split(f.Name)
+	r.dirindexes = append(r.dirindexes, r.di.Get(dir))
+	r.basenames = append(r.basenames, file)
+	r.fileowners = append(r.fileowners, f.Owner)
+	r.filegroups = append(r.filegroups, f.Group)
+	r.filemtimes = append(r.filemtimes, f.MTime)
+	r.fileflags = append(r.fileflags, uint32(f.Type))
+
+	links := 1
+	switch {
+	case f.Mode&0o40000 != 0: // directory
+		r.filesizes = append(r.filesizes, 4096)
+		r.filedigests = append(r.filedigests, "")
+		r.filelinktos = append(r.filelinktos, "")
+		links = 2
+	case f.Mode&0o120000 == 0o120000: //  symlink
+		r.filesizes = append(r.filesizes, uint32(len(f.Body)))
+		r.filedigests = append(r.filedigests, "")
+		r.filelinktos = append(r.filelinktos, string(f.Body))
+	default: // regular file
+		f.Mode = f.Mode | 0o100000
+		r.filesizes = append(r.filesizes, uint32(len(f.Body)))
+		r.filedigests = append(r.filedigests, fmt.Sprintf("%x", sha256.Sum256(f.Body)))
+		r.filelinktos = append(r.filelinktos, "")
+	}
+	r.filemodes = append(r.filemodes, uint16(f.Mode))
+
+	// Ghost files have no payload
+	if f.Type == GhostFile {
+		return nil
+	}
+	return r.writePayload(f, links)
+}
+
+func (r *RPM) writePayload(f RPMFile, links int) error {
+	hdr := &cpio.Header{
+		Name:  f.Name,
+		Mode:  cpio.FileMode(f.Mode),
+		Size:  int64(len(f.Body)),
+		Links: links,
+	}
+	if err := r.cpio.WriteHeader(hdr); err != nil {
+		return errors.Wrap(err, "failed to write payload file header")
+	}
+	if _, err := r.cpio.Write(f.Body); err != nil {
+		return errors.Wrap(err, "failed to write payload file content")
+	}
+	r.payloadSize += uint(len(f.Body))
+	return nil
+}
diff --git a/internal/rpmpack/sense.go b/internal/rpmpack/sense.go
new file mode 100644
index 0000000..cf97cad
--- /dev/null
+++ b/internal/rpmpack/sense.go
@@ -0,0 +1,168 @@
+package rpmpack
+
+import (
+	"fmt"
+	"regexp"
+	"strings"
+)
+
+type rpmSense uint32
+
+// https://github.com/rpm-software-management/rpm/blob/ab01b5eacf9ec6a07a5d9e1991ef476a12d264fd/include/rpm/rpmds.h#L27
+// SenseAny (0) specifies no specific version compare
+// SenseLess (2) specifies less then the specified version
+// SenseGreater (4) specifies greater then the specified version
+// SenseEqual (8) specifies equal to the specified version
+const (
+	SenseAny  rpmSense = 0
+	SenseLess          = 1 << iota
+	SenseGreater
+	SenseEqual
+	SenseRPMLIB rpmSense = 1 << 24
+)
+
+var relationMatch = regexp.MustCompile(`([^=<>\s]*)\s*((?:=|>|<)*)\s*(.*)?`)
+
+// Relation is the structure of rpm sense relationships
+type Relation struct {
+	Name    string
+	Version string
+	Sense   rpmSense
+}
+
+// String return the string representation of the Relation
+func (r *Relation) String() string {
+	return fmt.Sprintf("%s%v%s", r.Name, r.Sense, r.Version)
+}
+
+// Equal compare the equality of two relations
+func (r *Relation) Equal(o *Relation) bool {
+	return r.Name == o.Name && r.Version == o.Version && r.Sense == o.Sense
+}
+
+// Relations is a slice of Relation pointers
+type Relations []*Relation
+
+// String return the string representation of the Relations
+func (r *Relations) String() string {
+	var val []string
+	for _, rel := range *r {
+		val = append(val, rel.String())
+	}
+	return strings.Join(val, ",")
+}
+
+// Set parse a string into a Relation and append it to the Relations slice if it is missing
+// this is used by the flag package
+func (r *Relations) Set(value string) error {
+	relation, err := NewRelation(value)
+	if err != nil {
+		return err
+	}
+	r.addIfMissing(relation)
+
+	return nil
+}
+
+func (r *Relations) addIfMissing(value *Relation) {
+	for _, relation := range *r {
+		if relation.Equal(value) {
+			return
+		}
+	}
+
+	*r = append(*r, value)
+}
+
+// AddToIndex add the relations to the specified category on the index
+func (r *Relations) AddToIndex(h *index, nameTag, versionTag, flagsTag int) error {
+	var (
+		num      = len(*r)
+		names    = make([]string, num)
+		versions = make([]string, num)
+		flags    = make([]uint32, num)
+	)
+
+	if num == 0 {
+		return nil
+	}
+
+	for idx, relation := range *r {
+		names[idx] = relation.Name
+		versions[idx] = relation.Version
+		flags[idx] = uint32(relation.Sense)
+	}
+
+	h.Add(nameTag, EntryStringSlice(names))
+	h.Add(versionTag, EntryStringSlice(versions))
+	h.Add(flagsTag, EntryUint32(flags))
+
+	return nil
+}
+
+// NewRelation parse a string into a Relation
+func NewRelation(related string) (*Relation, error) {
+	var (
+		err   error
+		sense rpmSense
+		name,
+		version string
+	)
+
+	if strings.HasPrefix(related, "(") && strings.HasSuffix(related, ")") {
+		// This is a `rich` dependency which must be parsed at install time
+		// https://rpm-software-management.github.io/rpm/manual/boolean_dependencies.html
+		sense = SenseAny
+		name = related
+	} else {
+		parts := relationMatch.FindStringSubmatch(related)
+		if sense, err = parseSense(parts[2]); err != nil {
+			return nil, err
+		}
+		name = parts[1]
+		version = parts[3]
+	}
+
+	return &Relation{
+		Name:    name,
+		Version: version,
+		Sense:   sense,
+	}, nil
+}
+
+var stringToSense = map[string]rpmSense{
+	"":   SenseAny,
+	"<":  SenseLess,
+	">":  SenseGreater,
+	"=":  SenseEqual,
+	"<=": SenseLess | SenseEqual,
+	">=": SenseGreater | SenseEqual,
+}
+
+// String return the string representation of the rpmSense
+func (r rpmSense) String() string {
+	var (
+		val rpmSense
+		ret string
+	)
+
+	for ret, val = range stringToSense {
+		if r == val {
+			return ret
+		}
+	}
+
+	return "unknown"
+}
+
+func parseSense(sense string) (rpmSense, error) {
+	var (
+		ret rpmSense
+		ok  bool
+	)
+	if ret, ok = stringToSense[sense]; !ok {
+		return SenseAny, fmt.Errorf("unknown sense value: %s", sense)
+	}
+
+	return ret, nil
+}
diff --git a/internal/rpmpack/tags.go b/internal/rpmpack/tags.go
new file mode 100644
index 0000000..4359929
--- /dev/null
+++ b/internal/rpmpack/tags.go
@@ -0,0 +1,101 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package rpmpack
+
+// Define only tags which we actually use
+// https://github.com/rpm-software-management/rpm/blob/master/lib/rpmtag.h
+const (
+	tagHeaderI18NTable = 0x64 // 100
+	// Signature tags are obiously overlapping regular header tags..
+	sigRSA         = 0x010c // 256
+	sigSHA256      = 0x0111 // 273
+	sigSize        = 0x03e8 // 1000
+	sigPGP         = 0x03ea // 1002
+	sigPayloadSize = 0x03ef // 1007
+
+	// https://github.com/rpm-software-management/rpm/blob/92eadae94c48928bca90693ad63c46ceda37d81f/rpmio/rpmpgp.h#L258
+	hashAlgoSHA256 = 0x0008 // 8
+
+	tagName        = 0x03e8 // 1000
+	tagVersion     = 0x03e9 // 1001
+	tagRelease     = 0x03ea // 1002
+	tagEpoch       = 0x03eb // 1003
+	tagSummary     = 0x03ec // 1004
+	tagDescription = 0x03ed // 1005
+	tagBuildTime   = 0x03ee // 1006
+	tagBuildHost   = 0x03ef // 1007
+	tagSize        = 0x03f1 // 1009
+	tagVendor      = 0x03f3 // 1011
+	tagLicence     = 0x03f6 // 1014
+	tagPackager    = 0x03f7 // 1015
+	tagGroup       = 0x03f8 // 1016
+	tagURL         = 0x03fc // 1020
+	tagOS          = 0x03fd // 1021
+	tagArch        = 0x03fe // 1022
+
+	tagPrein  = 0x03ff // 1023
+	tagPostin = 0x0400 // 1024
+	tagPreun  = 0x0401 // 1025
+	tagPostun = 0x0402 // 1026
+
+	tagFileSizes         = 0x0404 // 1028
+	tagFileModes         = 0x0406 // 1030
+	tagFileRDevs         = 0x0409 // 1033
+	tagFileMTimes        = 0x040a // 1034
+	tagFileDigests       = 0x040b // 1035
+	tagFileLinkTos       = 0x040c // 1036
+	tagFileFlags         = 0x040d // 1037
+	tagFileUserName      = 0x040f // 1039
+	tagFileGroupName     = 0x0410 // 1040
+	tagSourceRPM         = 0x0414 // 1044
+	tagFileVerifyFlags   = 0x0415 // 1045
+	tagProvides          = 0x0417 // 1047
+	tagRequireFlags      = 0x0418 // 1048
+	tagRequires          = 0x0419 // 1049
+	tagRequireVersion    = 0x041a // 1050
+	tagConflictFlags     = 0x041d // 1053
+	tagConflicts         = 0x041e // 1054
+	tagConflictVersion   = 0x041f // 1055
+	tagPreinProg         = 0x043d // 1085
+	tagPostinProg        = 0x043e // 1086
+	tagPreunProg         = 0x043f // 1087
+	tagPostunProg        = 0x0440 // 1088
+	tagObsoletes         = 0x0442 // 1090
+	tagFileINodes        = 0x0448 // 1096
+	tagFileLangs         = 0x0449 // 1097
+	tagProvideFlags      = 0x0458 // 1112
+	tagProvideVersion    = 0x0459 // 1113
+	tagObsoleteFlags     = 0x045a // 1114
+	tagObsoleteVersion   = 0x045b // 1115
+	tagDirindexes        = 0x045c // 1116
+	tagBasenames         = 0x045d // 1117
+	tagDirnames          = 0x045e // 1118
+	tagPayloadFormat     = 0x0464 // 1124
+	tagPayloadCompressor = 0x0465 // 1125
+	tagPayloadFlags      = 0x0466 // 1126
+	tagPretrans          = 0x047f // 1151
+	tagPosttrans         = 0x0480 // 1152
+	tagPretransProg      = 0x0481 // 1153
+	tagPosttransProg     = 0x0482 // 1154
+	tagFileDigestAlgo    = 0x1393 // 5011
+	tagRecommends        = 0x13b6 // 5046
+	tagRecommendVersion  = 0x13b7 // 5047
+	tagRecommendFlags    = 0x13b8 // 5048
+	tagSuggests          = 0x13b9 // 5049
+	tagSuggestVersion    = 0x13ba // 5050
+	tagSuggestFlags      = 0x13bb // 5051
+	tagPayloadDigest     = 0x13e4 // 5092
+	tagPayloadDigestAlgo = 0x13e5 // 5093
+)
diff --git a/nfpm.go b/nfpm.go
index a5cc5e4..9f6301d 100644
--- a/nfpm.go
+++ b/nfpm.go
@@ -8,6 +8,7 @@ import (
 	"io"
 	"io/fs"
 	"os"
+	"strings"
 	"sync"
 
 	"github.com/AlekSi/pointer"
@@ -105,7 +106,7 @@ type Packager interface {
 // Config contains the top level configuration for packages.
 type Config struct {
 	Info           `yaml:",inline" json:",inline"`
-	Overrides      map[string]Overridables `yaml:"overrides,omitempty" json:"overrides,omitempty" jsonschema:"title=overrides,description=override some fields when packaging with a specific packager,enum=apk,enum=deb,enum=rpm"`
+	Overrides      map[string]*Overridables `yaml:"overrides,omitempty" json:"overrides,omitempty" jsonschema:"title=overrides,description=override some fields when packaging with a specific packager,enum=apk,enum=deb,enum=rpm"`
 	envMappingFunc func(string) string
 }
 
@@ -148,16 +149,37 @@ func (c *Config) Validate() error {
 	return nil
 }
 
+func remove(slice []string, s int) []string {
+	return append(slice[:s], slice[s+1:]...)
+}
+
+func (c *Config) expandEnvVarsStringSlice(items []string) []string {
+	for i, dep := range items {
+		val := strings.TrimSpace(os.Expand(dep, c.envMappingFunc))
+		items[i] = val
+	}
+	for i, val := range items {
+		if val == "" {
+			items = remove(items, i)
+		}
+	}
+
+	return items
+}
+
 func (c *Config) expandEnvVars() {
 	// Version related fields
 	c.Info.Release = os.Expand(c.Info.Release, c.envMappingFunc)
 	c.Info.Version = os.Expand(c.Info.Version, c.envMappingFunc)
 	c.Info.Prerelease = os.Expand(c.Info.Prerelease, c.envMappingFunc)
 	c.Info.Arch = os.Expand(c.Info.Arch, c.envMappingFunc)
-	for _, override := range c.Overrides {
-		for i, dep := range override.Depends {
-			override.Depends[i] = os.Expand(dep, c.envMappingFunc)
-		}
+	for or := range c.Overrides {
+		c.Overrides[or].Conflicts = c.expandEnvVarsStringSlice(c.Overrides[or].Conflicts)
+		c.Overrides[or].Depends = c.expandEnvVarsStringSlice(c.Overrides[or].Depends)
+		c.Overrides[or].Replaces = c.expandEnvVarsStringSlice(c.Overrides[or].Replaces)
+		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)
 	}
 
 	// Maintainer and vendor fields
diff --git a/rpm/rpm.go b/rpm/rpm.go
index bf33e99..fde0601 100644
--- a/rpm/rpm.go
+++ b/rpm/rpm.go
@@ -11,10 +11,10 @@ import (
 	"strings"
 	"time"
 
-	"github.com/google/rpmpack"
 	"github.com/goreleaser/chglog"
 	"github.com/goreleaser/nfpm/v2"
 	"github.com/goreleaser/nfpm/v2/files"
+	"github.com/goreleaser/nfpm/v2/internal/rpmpack"
 	"github.com/goreleaser/nfpm/v2/internal/sign"
 )
 
diff --git a/www/docs/configuration.md b/www/docs/configuration.md
index 7781e8f..7501d13 100644
--- a/www/docs/configuration.md
+++ b/www/docs/configuration.md
@@ -81,28 +81,46 @@ changelog: "changelog.yaml"
 disable_globbing: false
 
 # Packages it replaces. (overridable)
+# This will expand any env var you set in the field, eg ${REPLACE_BLA}
+# the env var approach can be used to account for differences in platforms
 replaces:
   - foobar
+  - ${REPLACE_BLA}
 
 # Packages it provides. (overridable)
+# This will expand any env var you set in the field, eg ${PROVIDES_BLA}
+# the env var approach can be used to account for differences in platforms
 provides:
   - bar
+  - ${PROVIDES_BLA}
 
 # Dependencies. (overridable)
+# This will expand any env var you set in the field, eg ${DEPENDS_NGINX}
+# the env var approach can be used to account for differences in platforms
+# eg rhel needs nginx >= 1:1.18 and deb needs nginx (>= 1.18.0)
 depends:
   - git
+  - ${DEPENDS_NGINX}
 
 # Recommended packages. (overridable)
+# This will expand any env var you set in the field, eg ${RECOMMENDS_BLA}
+# the env var approach can be used to account for differences in platforms
 recommends:
   - golang
+  - ${RECOMMENDS_BLA}
 
 # Suggested packages. (overridable)
+# This will expand any env var you set in the field, eg ${SUGGESTS_BLA}
+# the env var approach can be used to account for differences in platforms
 suggests:
   - bzr
 
 # Packages it conflicts with. (overridable)
+# This will expand any env var you set in the field, eg ${CONFLICTS_BLA}
+# the env var approach can be used to account for differences in platforms
 conflicts:
   - mercurial
+  - ${CONFLICTS_BLA}
 
 # Contents to add to the package
 # This can be binaries or any other files.


Test patch:
diff --git a/internal/rpmpack/dir_test.go b/internal/rpmpack/dir_test.go
new file mode 100644
index 0000000..1eadd8e
--- /dev/null
+++ b/internal/rpmpack/dir_test.go
@@ -0,0 +1,63 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package rpmpack
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestDirIndex(t *testing.T) {
+	testCases := []struct {
+		name        string
+		before      []string
+		dir         string
+		wantGet     uint32
+		wantAllDirs []string
+	}{{
+		name:        "first",
+		dir:         "/first",
+		wantGet:     0,
+		wantAllDirs: []string{"/first"},
+	}, {
+		name:        "second",
+		dir:         "second",
+		before:      []string{"first"},
+		wantGet:     1,
+		wantAllDirs: []string{"first", "second"},
+	}, {
+		name:        "repeat",
+		dir:         "second",
+		before:      []string{"first", "second", "third"},
+		wantGet:     1,
+		wantAllDirs: []string{"first", "second", "third"},
+	}}
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			d := newDirIndex()
+			for _, b := range tc.before {
+				d.Get(b)
+			}
+			if got := d.Get(tc.dir); got != tc.wantGet {
+				t.Errorf("d.Get(%q) = %d, want: %d", tc.dir, got, tc.wantGet)
+			}
+			if df := cmp.Diff(tc.wantAllDirs, d.AllDirs()); df != "" {
+				t.Errorf("d.AllDirs() diff (want->got):\n%s", df)
+			}
+		})
+	}
+}
diff --git a/internal/rpmpack/file_types_test.go b/internal/rpmpack/file_types_test.go
new file mode 100644
index 0000000..76f4c79
--- /dev/null
+++ b/internal/rpmpack/file_types_test.go
@@ -0,0 +1,32 @@
+package rpmpack
+
+import (
+	"testing"
+)
+
+func TestFileTypeSetting(t *testing.T) {
+	f := &RPMFile{
+		Name: "Test",
+	}
+
+	if f.Type != GenericFile {
+		t.Error("New RPMFile.Type should be a generic type")
+	}
+
+	f.Type |= ConfigFile
+	if (f.Type & ConfigFile) == 0 {
+		t.Error("Setting to config file should have the ConfigFile bitmask")
+	}
+}
+
+func TestFileTypeCombining(t *testing.T) {
+	f := &RPMFile{
+		Name: "Test",
+	}
+
+	f.Type |= ConfigFile | NoReplaceFile
+
+	if (f.Type&ConfigFile) == 0 || f.Type&NoReplaceFile == 0 {
+		t.Error("Combining file types should have the bitmask of both")
+	}
+}
diff --git a/internal/rpmpack/header_test.go b/internal/rpmpack/header_test.go
new file mode 100644
index 0000000..2d45a61
--- /dev/null
+++ b/internal/rpmpack/header_test.go
@@ -0,0 +1,114 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package rpmpack
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestLead(t *testing.T) {
+	// Only check that the length is always right
+	names := []string{
+		"a",
+		"ab",
+		"abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc",
+		"abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabca",
+		"abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab",
+		"abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc",
+	}
+	for _, n := range names {
+		if got := len(lead(n, "1-2")); got != 0x60 {
+			t.Errorf("len(lead(%s)) = %#x, want %#x", n, got, 0x60)
+		}
+	}
+}
+
+func TestEntry(t *testing.T) {
+	testCases := []struct {
+		name           string
+		value          interface{}
+		tag            int
+		offset         int
+		wantIndexBytes string
+		wantData       string
+	}{{
+		name:           "simple int",
+		value:          []int32{0x42},
+		tag:            0x010d,
+		offset:         5,
+		wantIndexBytes: "0000010d000000040000000500000001",
+		wantData:       "00000042",
+	}, {
+		name:           "simple string",
+		value:          "simple string",
+		tag:            0x010e,
+		offset:         0x111,
+		wantIndexBytes: "0000010e000000060000011100000001",
+		wantData:       "73696d706c6520737472696e6700",
+	}, {
+		name:           "string array",
+		value:          []string{"string", "array"},
+		tag:            0x010f,
+		offset:         0x222,
+		wantIndexBytes: "0000010f000000080000022200000002",
+		wantData:       "737472696e6700617272617900",
+	}}
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			var e IndexEntry
+			switch v := tc.value.(type) {
+			case []string:
+				e = EntryStringSlice(v)
+			case string:
+				e = EntryString(v)
+			case []int32:
+				e = EntryInt32(v)
+			}
+			gotBytes := e.indexBytes(tc.tag, tc.offset)
+			if d := cmp.Diff(tc.wantIndexBytes, fmt.Sprintf("%x", gotBytes)); d != "" {
+				t.Errorf("entry.indexBytes() unexpected value (want->got):\n%s", d)
+			}
+			if d := cmp.Diff(tc.wantData, fmt.Sprintf("%x", e.data)); d != "" {
+				t.Errorf("entry.data unexpected value (want->got):\n%s", d)
+			}
+		})
+	}
+}
+
+func TestIndex(t *testing.T) {
+	i := newIndex(0x3e)
+	i.AddEntries(map[int]IndexEntry{
+		0x1111: EntryUint16([]uint16{0x4444, 0x8888, 0xcccc}),
+		0x2222: EntryUint32([]uint32{0x3333, 0x5555}),
+	})
+	got, err := i.Bytes()
+	if err != nil {
+		t.Errorf("i.Bytes() returned error: %v", err)
+	}
+	want := "8eade80100000000" + // header lead
+		"0000000300000020" + // count and size
+		"0000003e000000070000001000000010" + // eigen header entry
+		"00001111000000030000000000000003" +
+		"00002222000000040000000800000002" +
+		"44448888cccc00000000333300005555" + // values, with padding
+		"0000003e00000007ffffffd000000010" // eigen header value
+	if d := cmp.Diff(want, fmt.Sprintf("%x", got)); d != "" {
+		t.Errorf("i.Bytes() unexpected value (want-> got): \n%s", d)
+	}
+}
diff --git a/internal/rpmpack/rpm_test.go b/internal/rpmpack/rpm_test.go
new file mode 100644
index 0000000..f21597a
--- /dev/null
+++ b/internal/rpmpack/rpm_test.go
@@ -0,0 +1,182 @@
+package rpmpack
+
+import (
+	"io"
+	"reflect"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+
+	"github.com/klauspost/compress/zstd"
+	gzip "github.com/klauspost/pgzip"
+	"github.com/ulikunitz/xz"
+	"github.com/ulikunitz/xz/lzma"
+)
+
+func TestFileOwner(t *testing.T) {
+	r, err := NewRPM(RPMMetaData{})
+	if err != nil {
+		t.Fatalf("NewRPM returned error %v", err)
+	}
+	group := "testGroup"
+	user := "testUser"
+
+	r.AddFile(RPMFile{
+		Name:  "/usr/local/hello",
+		Body:  []byte("content of the file"),
+		Group: group,
+		Owner: user,
+	})
+
+	if err := r.Write(io.Discard); err != nil {
+		t.Errorf("NewRPM returned error %v", err)
+	}
+	if r.fileowners[0] != user {
+		t.Errorf("File owner shoud be %s but is %s", user, r.fileowners[0])
+	}
+	if r.filegroups[0] != group {
+		t.Errorf("File owner shoud be %s but is %s", group, r.filegroups[0])
+	}
+}
+
+// https://github.com/google/rpmpack/issues/49
+func Test100644(t *testing.T) {
+	r, err := NewRPM(RPMMetaData{})
+	if err != nil {
+		t.Fatalf("NewRPM returned error %v", err)
+	}
+	r.AddFile(RPMFile{
+		Name: "/usr/local/hello",
+		Body: []byte("content of the file"),
+		Mode: 0o100644,
+	})
+
+	if err := r.Write(io.Discard); err != nil {
+		t.Errorf("Write returned error %v", err)
+	}
+	if r.filemodes[0] != 0o100644 {
+		t.Errorf("file mode want 0100644, got %o", r.filemodes[0])
+	}
+	if r.filelinktos[0] != "" {
+		t.Errorf("linktos want empty (not a symlink), got %q", r.filelinktos[0])
+	}
+}
+
+func TestCompression(t *testing.T) {
+	testCases := []struct {
+		Type           string
+		Compressors    []string
+		ExpectedWriter io.Writer
+	}{
+		{
+			Type: "gzip",
+			Compressors: []string{
+				"", "gzip", "gzip:1", "gzip:2", "gzip:3",
+				"gzip:4", "gzip:5", "gzip:6", "gzip:7", "gzip:8", "gzip:9",
+			},
+			ExpectedWriter: &gzip.Writer{},
+		},
+		{
+			Type:           "gzip",
+			Compressors:    []string{"gzip:fast", "gzip:10"},
+			ExpectedWriter: nil, // gzip requires an integer level from -2 to 9
+		},
+		{
+			Type:           "lzma",
+			Compressors:    []string{"lzma"},
+			ExpectedWriter: &lzma.Writer{},
+		},
+		{
+			Type:           "lzma",
+			Compressors:    []string{"lzma:fast", "lzma:1"},
+			ExpectedWriter: nil, // lzma does not support specifying the compression level
+		},
+		{
+			Type:           "xz",
+			Compressors:    []string{"xz"},
+			ExpectedWriter: &xz.Writer{},
+		},
+		{
+			Type:           "xz",
+			Compressors:    []string{"xz:fast", "xz:1"},
+			ExpectedWriter: nil, // xz does not support specifying the compression level
+		},
+		{
+			Type: "zstd",
+			Compressors: []string{
+				"zstd", "zstd:fastest", "zstd:default", "zstd:better",
+				"zstd:best", "zstd:BeSt", "zstd:0", "zstd:4", "zstd:8", "zstd:15",
+			},
+			ExpectedWriter: &zstd.Encoder{},
+		},
+		{
+			Type:           "zstd",
+			Compressors:    []string{"xz:worst"},
+			ExpectedWriter: nil, // only integers levels or one of the pre-defined string values
+		},
+	}
+
+	for _, testCase := range testCases {
+		testCase := testCase
+
+		for _, compressor := range testCase.Compressors {
+			t.Run(compressor, func(t *testing.T) {
+				r, err := NewRPM(RPMMetaData{
+					Compressor: compressor,
+				})
+				if err != nil {
+					if testCase.ExpectedWriter == nil {
+						return // an error is expected
+					}
+
+					t.Fatalf("NewRPM returned error %v", err)
+				}
+
+				if testCase.ExpectedWriter == nil {
+					t.Fatalf("compressor %q should have produced an error", compressor)
+				}
+
+				if r.RPMMetaData.Compressor != testCase.Type {
+					t.Fatalf("expected compressor %q, got %q", compressor,
+						r.RPMMetaData.Compressor)
+				}
+
+				expectedWriterType := reflect.Indirect(reflect.ValueOf(
+					testCase.ExpectedWriter)).String()
+				actualWriterType := reflect.Indirect(reflect.ValueOf(
+					r.compressedPayload)).String()
+
+				if expectedWriterType != actualWriterType {
+					t.Fatalf("expected writer to be %T, got %T instead",
+						testCase.ExpectedWriter, r.compressedPayload)
+				}
+			})
+		}
+	}
+}
+
+func TestAllowListDirs(t *testing.T) {
+	r, err := NewRPM(RPMMetaData{})
+	if err != nil {
+		t.Fatalf("NewRPM returned error %v", err)
+	}
+
+	r.AddFile(RPMFile{
+		Name: "/usr/local/dir1",
+		Mode: 0o40000,
+	})
+	r.AddFile(RPMFile{
+		Name: "/usr/local/dir2",
+		Mode: 0o40000,
+	})
+
+	r.AllowListDirs(map[string]bool{"/usr/local/dir1": true})
+
+	if err := r.Write(io.Discard); err != nil {
+		t.Errorf("NewRPM returned error %v", err)
+	}
+	expected := map[string]RPMFile{"/usr/local/dir1": {Name: "/usr/local/dir1", Mode: 0o40000}}
+	if d := cmp.Diff(expected, r.files); d != "" {
+		t.Errorf("Expected dirs differs (want->got):\n%v", d)
+	}
+}
diff --git a/internal/rpmpack/sense_test.go b/internal/rpmpack/sense_test.go
new file mode 100644
index 0000000..17f7620
--- /dev/null
+++ b/internal/rpmpack/sense_test.go
@@ -0,0 +1,81 @@
+package rpmpack
+
+import (
+	"testing"
+)
+
+func TestNewRelation(t *testing.T) {
+	testCases := []struct {
+		input, output string
+		errExpected   bool
+	}{
+		{
+			input:  "python >= 3.7",
+			output: "python>=3.7",
+		},
+		{
+			input:  "python",
+			output: "python",
+		},
+		{
+			input:  "python=2",
+			output: "python=2",
+		},
+		{
+			input:  "python >=3.5",
+			output: "python>=3.5",
+		},
+		{
+			input:       "python >< 3.5",
+			output:      "",
+			errExpected: true,
+		},
+		{
+			input:       "python <> 3.5",
+			output:      "",
+			errExpected: true,
+		},
+		{
+			input:       "python == 3.5",
+			output:      "",
+			errExpected: true,
+		},
+		{
+			input:       "python =< 3.5",
+			output:      "",
+			errExpected: true,
+		},
+		{
+			input:       "python => 3.5",
+			output:      "",
+			errExpected: true,
+		},
+	}
+
+	for _, tc := range testCases {
+		testCase := tc
+		t.Run(testCase.input, func(tt *testing.T) {
+			relation, err := NewRelation(testCase.input)
+			switch {
+			case testCase.errExpected && err == nil:
+				tt.Errorf("%s should have returned an error", testCase.input)
+				return
+			case !testCase.errExpected && err != nil:
+				tt.Errorf("%s should not have returned an error: %v", testCase.input, err)
+				return
+			case testCase.errExpected && err != nil:
+				return
+			}
+
+			if relation == nil {
+				tt.Errorf("%s should not have returned a nil relation", testCase.input)
+				return
+			}
+
+			val := relation.String()
+			if !testCase.errExpected && val != testCase.output {
+				tt.Errorf("%s should have returned %s not %s", testCase.input, testCase.output, val)
+			}
+		})
+	}
+}
diff --git a/nfpm_test.go b/nfpm_test.go
index 37799c9..437dbbe 100644
--- a/nfpm_test.go
+++ b/nfpm_test.go
@@ -153,7 +153,7 @@ func TestValidate(t *testing.T) {
 }
 
 func TestValidateError(t *testing.T) {
-	for err, info := range map[string]nfpm.Info{
+	for err, info := range map[string]*nfpm.Info{
 		"package name must be provided": {},
 		"package arch must be provided": {
 			Name: "fo",
@@ -163,11 +163,11 @@ func TestValidateError(t *testing.T) {
 			Arch: "asd",
 		},
 	} {
-		err := err
-		info := info
-		t.Run(err, func(t *testing.T) {
-			require.EqualError(t, nfpm.Validate(&info), err)
-		})
+		func(inf *nfpm.Info, e string) {
+			t.Run(e, func(t *testing.T) {
+				require.EqualError(t, nfpm.Validate(inf), e)
+			})
+		}(info, err)
 	}
 }
 
@@ -340,6 +340,28 @@ overrides:
 		require.Len(t, info.Overrides["rpm"].Depends, 1)
 		require.Equal(t, "package = 1.0.0", info.Overrides["rpm"].Depends[0])
 	})
+
+	t.Run("depends-strips-empty", func(t *testing.T) {
+		os.Clearenv()
+		os.Setenv("VERSION", version)
+		os.Setenv("PKG", "")
+		info, err := nfpm.Parse(strings.NewReader(`---
+name: foo
+overrides:
+  deb:
+    depends:
+    - ${PKG}
+    - package (= ${VERSION})
+  rpm:
+    depends:
+    - package = ${VERSION}
+    - ${PKG}`))
+		require.NoError(t, err)
+		require.Len(t, info.Overrides["deb"].Depends, 1)
+		require.Equal(t, "package (= 1.0.0)", info.Overrides["deb"].Depends[0])
+		require.Len(t, info.Overrides["rpm"].Depends, 1)
+		require.Equal(t, "package = 1.0.0", info.Overrides["rpm"].Depends[0])
+	})
 }
 
 func TestOverrides(t *testing.T) {
diff --git a/testdata/acceptance/core.overrides.yaml b/testdata/acceptance/core.overrides.yaml
index 0f31da9..32177fc 100644
--- a/testdata/acceptance/core.overrides.yaml
+++ b/testdata/acceptance/core.overrides.yaml
@@ -27,14 +27,20 @@ contents:
   type: config
 overrides:
   rpm:
+    depends:
+      - (bash >= 4.4 or fish)
     scripts:
       preinstall: ./testdata/acceptance/scripts/preinstall.sh
       postremove: ./testdata/acceptance/scripts/postremove.sh
   deb:
+    depends:
+      - bash (>= 4.4) | fish
     scripts:
       postinstall: ./testdata/acceptance/scripts/postinstall.sh
       preremove: ./testdata/acceptance/scripts/preremove.sh
   apk:
+    depends:
+      - bash (>= 4.4) | fish
     scripts:
       postinstall: ./testdata/acceptance/scripts/postinstall.sh
       preremove: ./testdata/acceptance/scripts/preremove.sh