mirror of
https://github.com/goreleaser/nfpm
synced 2024-05-24 18:26:13 +02:00
feat: support symlinks for deb and rpm. (#185)
* feat: Support symlinks. * fix: Fix symlink creation for deb. * fix: Remove magic number in rpm symlink creation. * test: Add symlink unit tests. * test: Add symlink acceptance tests. * doc: Add documentation for symlinks. * fix: Fix acceptance test name. * fix: Make symlink headers more consistent.
This commit is contained in:
parent
42f29b9d7a
commit
0000a2fe8d
|
@ -210,6 +210,21 @@ func TestDebTriggers(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestSymlink(t *testing.T) {
|
||||
for _, format := range formats {
|
||||
format := format
|
||||
t.Run(fmt.Sprintf("symlink-%s", format), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
accept(t, acceptParms{
|
||||
Name: fmt.Sprintf("symlink_%s", format),
|
||||
Conf: "symlink.yaml",
|
||||
Format: format,
|
||||
Dockerfile: fmt.Sprintf("%s.symlink.dockerfile", format),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type acceptParms struct {
|
||||
Name string
|
||||
Conf string
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
FROM ubuntu
|
||||
ARG package
|
||||
COPY ${package} /tmp/foo.deb
|
||||
RUN dpkg -i /tmp/foo.deb
|
||||
RUN ls -l /path/to/symlink | grep "/path/to/symlink -> /etc/foo/whatever.conf"
|
||||
RUN dpkg -r foo
|
|
@ -0,0 +1,6 @@
|
|||
FROM fedora
|
||||
ARG package
|
||||
COPY ${package} /tmp/foo.rpm
|
||||
RUN rpm -ivh /tmp/foo.rpm
|
||||
RUN ls -l /path/to/symlink | grep "/path/to/symlink -> /etc/foo/whatever.conf"
|
||||
RUN rpm -e foo
|
|
@ -0,0 +1,12 @@
|
|||
name: "foo"
|
||||
arch: "amd64"
|
||||
platform: "linux"
|
||||
version: "v1.2.3"
|
||||
maintainer: "Foo Bar"
|
||||
vendor: "foobar"
|
||||
homepage: "https://foobar.org"
|
||||
license: "MIT"
|
||||
files:
|
||||
../testdata/whatever.conf: "/etc/foo/whatever.conf"
|
||||
symlinks:
|
||||
/path/to/symlink: "/etc/foo/whatever.conf"
|
|
@ -154,6 +154,8 @@ files:
|
|||
./bar: "/usr/local/bin/bar"
|
||||
config_files:
|
||||
./foobar.conf: "/etc/foobar.conf"
|
||||
symlinks:
|
||||
/sbin/foo: "/usr/local/bin/foo"
|
||||
overrides:
|
||||
rpm:
|
||||
scripts:
|
||||
|
|
25
deb/deb.go
25
deb/deb.go
|
@ -131,6 +131,10 @@ func createDataTarGz(info *nfpm.Info) (dataTarGz, md5sums []byte, instSize int64
|
|||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
if err := createSymlinksInsideTarGz(info, out, created); err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
if err := out.Close(); err != nil {
|
||||
return nil, nil, 0, errors.Wrap(err, "closing data.tar.gz")
|
||||
}
|
||||
|
@ -141,6 +145,27 @@ func createDataTarGz(info *nfpm.Info) (dataTarGz, md5sums []byte, instSize int64
|
|||
return buf.Bytes(), md5buf.Bytes(), instSize, nil
|
||||
}
|
||||
|
||||
func createSymlinksInsideTarGz(info *nfpm.Info, out *tar.Writer, created map[string]bool) error {
|
||||
for src, dst := range info.Symlinks {
|
||||
if err := createTree(out, src, created); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := newItemInsideTarGz(out, []byte{}, &tar.Header{
|
||||
Name: src,
|
||||
Linkname: dst,
|
||||
Typeflag: tar.TypeSymlink,
|
||||
ModTime: time.Now(),
|
||||
Format: tar.FormatGNU,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createFilesInsideTarGz(info *nfpm.Info, out *tar.Writer, created map[string]bool) (bytes.Buffer, int64, error) {
|
||||
var md5buf bytes.Buffer
|
||||
var instSize int64
|
||||
|
|
|
@ -591,6 +591,39 @@ func TestSymlinkInFiles(t *testing.T) {
|
|||
assert.Equal(t, string(realSymlinkTarget), string(packagedSymlinkTarget))
|
||||
}
|
||||
|
||||
func TestSymlink(t *testing.T) {
|
||||
var (
|
||||
configFilePath = "/usr/share/doc/fake/fake.txt"
|
||||
symlink = "/path/to/symlink"
|
||||
symlinkTarget = configFilePath
|
||||
)
|
||||
|
||||
info := &nfpm.Info{
|
||||
Name: "symlink-in-files",
|
||||
Arch: "amd64",
|
||||
Description: "This package's config references a file via symlink.",
|
||||
Version: "1.0.0",
|
||||
Overridables: nfpm.Overridables{
|
||||
Files: map[string]string{
|
||||
"../testdata/whatever.conf": configFilePath,
|
||||
},
|
||||
Symlinks: map[string]string{
|
||||
symlink: symlinkTarget,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dataTarGz, _, _, err := createDataTarGz(info)
|
||||
assert.NoError(t, err)
|
||||
|
||||
packagedSymlinkHeader, err := extractFileHeaderFromTarGz(dataTarGz, symlink)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, symlink, packagedSymlinkHeader.Name)
|
||||
assert.Equal(t, uint8(tar.TypeSymlink), packagedSymlinkHeader.Typeflag)
|
||||
assert.Equal(t, symlinkTarget, packagedSymlinkHeader.Linkname)
|
||||
}
|
||||
|
||||
func extractFileFromTarGz(tarGzFile []byte, filename string) ([]byte, error) {
|
||||
tarFile, err := gzipInflate(tarGzFile)
|
||||
if err != nil {
|
||||
|
@ -622,6 +655,32 @@ func extractFileFromTarGz(tarGzFile []byte, filename string) ([]byte, error) {
|
|||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func extractFileHeaderFromTarGz(tarGzFile []byte, filename string) (*tar.Header, error) {
|
||||
tarFile, err := gzipInflate(tarGzFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tr := tar.NewReader(bytes.NewReader(tarFile))
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break // End of archive
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if path.Join("/", hdr.Name) != path.Join("/", filename) {
|
||||
continue
|
||||
}
|
||||
|
||||
return hdr, nil
|
||||
}
|
||||
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func gzipInflate(data []byte) ([]byte, error) {
|
||||
gzr, err := gzip.NewReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
|
|
1
nfpm.go
1
nfpm.go
|
@ -153,6 +153,7 @@ type Overridables struct {
|
|||
Conflicts []string `yaml:"conflicts,omitempty"`
|
||||
Files map[string]string `yaml:"files,omitempty"`
|
||||
ConfigFiles map[string]string `yaml:"config_files,omitempty"`
|
||||
Symlinks map[string]string `yaml:"symlinks,omitempty"`
|
||||
EmptyFolders []string `yaml:"empty_folders,omitempty"`
|
||||
Scripts Scripts `yaml:"scripts,omitempty"`
|
||||
RPM RPM `yaml:"rpm,omitempty"`
|
||||
|
|
16
rpm/rpm.go
16
rpm/rpm.go
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
"github.com/google/rpmpack"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sassoftware/go-rpmutils/cpio"
|
||||
|
||||
"github.com/goreleaser/chglog"
|
||||
|
||||
|
@ -103,6 +104,8 @@ func (*RPM) Package(info *nfpm.Info, w io.Writer) error {
|
|||
return err
|
||||
}
|
||||
|
||||
addSymlinksInsideRPM(info, rpm)
|
||||
|
||||
if err = addScriptFiles(info, rpm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -315,6 +318,19 @@ func createFilesInsideRPM(info *nfpm.Info, rpm *rpmpack.RPM) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func addSymlinksInsideRPM(info *nfpm.Info, rpm *rpmpack.RPM) {
|
||||
for src, dst := range info.Symlinks {
|
||||
rpm.AddFile(rpmpack.RPMFile{
|
||||
Name: src,
|
||||
Body: []byte(dst),
|
||||
Mode: uint(cpio.S_ISLNK),
|
||||
MTime: uint32(time.Now().UTC().Unix()),
|
||||
Owner: "root",
|
||||
Group: "root",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func copyToRPM(rpm *rpmpack.RPM, src, dst string, config bool) error {
|
||||
file, err := os.OpenFile(src, os.O_RDONLY, 0600) //nolint:gosec
|
||||
if err != nil {
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/sassoftware/go-rpmutils"
|
||||
"github.com/sassoftware/go-rpmutils/cpio"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/goreleaser/chglog"
|
||||
|
@ -435,6 +436,43 @@ func TestSymlinkInFiles(t *testing.T) {
|
|||
assert.Equal(t, string(realSymlinkTarget), string(packagedSymlinkTarget))
|
||||
}
|
||||
|
||||
func TestSymlink(t *testing.T) {
|
||||
var (
|
||||
configFilePath = "/usr/share/doc/fake/fake.txt"
|
||||
symlink = "/path/to/symlink"
|
||||
symlinkTarget = configFilePath
|
||||
)
|
||||
|
||||
info := &nfpm.Info{
|
||||
Name: "symlink-in-files",
|
||||
Arch: "amd64",
|
||||
Description: "This package's config references a file via symlink.",
|
||||
Version: "1.0.0",
|
||||
Overridables: nfpm.Overridables{
|
||||
Files: map[string]string{
|
||||
"../testdata/whatever.conf": configFilePath,
|
||||
},
|
||||
Symlinks: map[string]string{
|
||||
symlink: symlinkTarget,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var rpmFileBuffer bytes.Buffer
|
||||
err := Default.Package(info, &rpmFileBuffer)
|
||||
assert.NoError(t, err)
|
||||
|
||||
packagedSymlinkHeader, err := extractFileHeaderFromRpm(rpmFileBuffer.Bytes(), symlink)
|
||||
assert.NoError(t, err)
|
||||
|
||||
packagedSymlink, err := extractFileFromRpm(rpmFileBuffer.Bytes(), symlink)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, symlink, packagedSymlinkHeader.Filename())
|
||||
assert.Equal(t, cpio.S_ISLNK, packagedSymlinkHeader.Mode())
|
||||
assert.Equal(t, symlinkTarget, string(packagedSymlink))
|
||||
}
|
||||
|
||||
func extractFileFromRpm(rpm []byte, filename string) ([]byte, error) {
|
||||
rpmFile, err := rpmutils.ReadRpm(bytes.NewReader(rpm))
|
||||
if err != nil {
|
||||
|
@ -470,6 +508,36 @@ func extractFileFromRpm(rpm []byte, filename string) ([]byte, error) {
|
|||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func extractFileHeaderFromRpm(rpm []byte, filename string) (*cpio.Cpio_newc_header, error) {
|
||||
rpmFile, err := rpmutils.ReadRpm(bytes.NewReader(rpm))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pr, err := rpmFile.PayloadReader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for {
|
||||
hdr, err := pr.Next()
|
||||
if err == io.EOF {
|
||||
break // End of archive
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hdr.Filename() != filename {
|
||||
continue
|
||||
}
|
||||
|
||||
return hdr, nil
|
||||
}
|
||||
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func symlinkTo(tb testing.TB, fileName string) string {
|
||||
target, err := filepath.Abs(fileName)
|
||||
assert.NoError(tb, err)
|
||||
|
|
|
@ -97,6 +97,10 @@ config_files:
|
|||
empty_folders:
|
||||
- /var/log/foo
|
||||
|
||||
# Symlinks mapping from symlink name inside package to target inside package (overridable)
|
||||
symlinks:
|
||||
/sbin/foo: /usr/local/bin/foo
|
||||
|
||||
# Scripts to run at specific stages. (overridable)
|
||||
scripts:
|
||||
preinstall: ./scripts/preinstall.sh
|
||||
|
|
Loading…
Reference in New Issue