diff --git a/pipeline/docker/docker.go b/pipeline/docker/docker.go index b1612c401..b94b4c448 100644 --- a/pipeline/docker/docker.go +++ b/pipeline/docker/docker.go @@ -16,6 +16,7 @@ import ( "github.com/goreleaser/goreleaser/context" "github.com/goreleaser/goreleaser/internal/artifact" "github.com/goreleaser/goreleaser/pipeline" + "io/ioutil" ) // ErrNoDocker is shown when docker cannot be found in $PATH @@ -127,7 +128,7 @@ func process(ctx *context.Context, docker config.Docker, artifact artifact.Artif return errors.Wrap(err, "failed to link dockerfile") } for _, file := range docker.Files { - if err := os.Link(file, filepath.Join(root, filepath.Base(file))); err != nil { + if err := link(file, filepath.Join(root, filepath.Base(file))); err != nil { return errors.Wrapf(err, "failed to link extra file '%s'", file) } } @@ -143,6 +144,62 @@ func process(ctx *context.Context, docker config.Docker, artifact artifact.Artif return publish(ctx, docker, image, latest) } +// link a file or directory hard +func link(src, dest string) error { + info, err := os.Stat(src) + if err != nil { + return err + } + if info.IsDir() { + return directoryLink(src, dest, info) + } + return fileLink(src, dest) +} + +// directoryLink recursively creates all subdirectories and links all files hard +func directoryLink(src, dest string, info os.FileInfo) error { + if info == nil { + i, err := os.Stat(src) + if err != nil { + return err + } + info = i + } + if err := os.MkdirAll(dest, info.Mode()); err != nil { + return err + } + infos, err := ioutil.ReadDir(src) + if err != nil { + return err + } + for _, info := range infos { + if info.IsDir() { + err := directoryLink( + filepath.Join(src, info.Name()), + filepath.Join(dest, info.Name()), + info, + ) + if err != nil { + return err + } + } else { + err := fileLink( + filepath.Join(src, info.Name()), + filepath.Join(dest, info.Name()), + ) + if err != nil { + return err + } + } + } + return nil +} + +// fileLink links a file hard +func fileLink(src, dest string) error { + return os.Link(src, dest) +} + func publish(ctx *context.Context, docker config.Docker, image, latest string) error { // TODO: improve this so it can log it to stdout if !ctx.Publish { diff --git a/pipeline/docker/docker_test.go b/pipeline/docker/docker_test.go index a8cd1f493..31d151cbb 100644 --- a/pipeline/docker/docker_test.go +++ b/pipeline/docker/docker_test.go @@ -12,6 +12,7 @@ import ( "github.com/goreleaser/goreleaser/internal/artifact" "github.com/goreleaser/goreleaser/pipeline" "github.com/stretchr/testify/assert" + "syscall" ) func killAndRm(t *testing.T) { @@ -236,3 +237,60 @@ func TestDefaultSet(t *testing.T) { assert.Equal(t, "{{ .Version }}", docker.TagTemplate) assert.Equal(t, "Dockerfile.foo", docker.Dockerfile) } + +func TestLinkFile(t *testing.T) { + const srcFile = "/tmp/test" + const dstFile = "/tmp/linked" + err := ioutil.WriteFile(srcFile, []byte("foo"), 0644) + if err != nil { + t.Log("Cannot setup test file") + t.Fail() + } + err = link(srcFile, dstFile) + if err != nil { + t.Log("Failed to link: ", err) + t.Fail() + } + if inode(srcFile) != inode(dstFile) { + t.Log("Inodes do not match, destination file is not a link") + t.Fail() + } + // cleanup + os.Remove(srcFile) + os.Remove(dstFile) +} + +func TestLinkDirectory(t *testing.T) { + const srcDir = "/tmp/testdir" + const testFile = "test" + const dstDir = "/tmp/linkedDir" + + os.Mkdir(srcDir, 0755) + err := ioutil.WriteFile(srcDir+"/"+testFile, []byte("foo"), 0644) + if err != nil { + t.Log("Cannot setup test file") + t.Fail() + } + err = directoryLink(srcDir, dstDir, nil) + if err != nil { + t.Log("Failed to link: ", err) + t.Fail() + } + if inode(srcDir+"/"+testFile) != inode(dstDir+"/"+testFile) { + t.Log("Inodes do not match, destination file is not a link") + t.Fail() + } + + // cleanup + os.RemoveAll(srcDir) + os.RemoveAll(dstDir) +} + +func inode(file string) uint64 { + fileInfo, err := os.Stat(file) + if err != nil { + return 0 + } + stat := fileInfo.Sys().(*syscall.Stat_t) + return stat.Ino +}