Skip to content

Commit d33a4d0

Browse files
committed
Handle recursive links in runtime sources.
1 parent fbf7ca2 commit d33a4d0

File tree

5 files changed

+104
-13
lines changed

5 files changed

+104
-13
lines changed

internal/smartlink/smartlink.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
)
1111

1212
// LinkContents will link the contents of src to desc
13-
func LinkContents(src, dest string) error {
13+
func LinkContents(src, dest string, visited map[string]bool) error {
1414
if !fileutils.DirExists(src) {
1515
return errs.New("src dir does not exist: %s", src)
1616
}
@@ -24,12 +24,23 @@ func LinkContents(src, dest string) error {
2424
return errs.Wrap(err, "Could not resolve src and dest paths")
2525
}
2626

27+
if visited == nil {
28+
visited = make(map[string]bool)
29+
}
30+
if _, exists := visited[src]; exists {
31+
// We've encountered a recursive link. This is most often the case when the resolved src has
32+
// already been visited. In that case, just link the dest to the src (which may be a directory;
33+
// this is fine).
34+
return linkFile(src, dest)
35+
}
36+
visited[src] = true
37+
2738
entries, err := os.ReadDir(src)
2839
if err != nil {
2940
return errs.Wrap(err, "Reading dir %s failed", src)
3041
}
3142
for _, entry := range entries {
32-
if err := Link(filepath.Join(src, entry.Name()), filepath.Join(dest, entry.Name())); err != nil {
43+
if err := Link(filepath.Join(src, entry.Name()), filepath.Join(dest, entry.Name()), visited); err != nil {
3344
return errs.Wrap(err, "Link failed")
3445
}
3546
}
@@ -39,13 +50,24 @@ func LinkContents(src, dest string) error {
3950

4051
// Link creates a link from src to target. MS decided to support Symlinks but only if you opt into developer mode (go figure),
4152
// which we cannot reasonably force on our users. So on Windows we will instead create dirs and hardlinks.
42-
func Link(src, dest string) error {
53+
func Link(src, dest string, visited map[string]bool) error {
4354
var err error
4455
src, dest, err = resolvePaths(src, dest)
4556
if err != nil {
4657
return errs.Wrap(err, "Could not resolve src and dest paths")
4758
}
4859

60+
if visited == nil {
61+
visited = make(map[string]bool)
62+
}
63+
if _, exists := visited[src]; exists {
64+
// We've encountered a recursive link. This is most often the case when the resolved src has
65+
// already been visited. In that case, just link the dest to the src (which may be a directory;
66+
// this is fine).
67+
return linkFile(src, dest)
68+
}
69+
visited[src] = true
70+
4971
if fileutils.IsDir(src) {
5072
if err := fileutils.Mkdir(dest); err != nil {
5173
return errs.Wrap(err, "could not create directory %s", dest)
@@ -55,7 +77,7 @@ func Link(src, dest string) error {
5577
return errs.Wrap(err, "could not read directory %s", src)
5678
}
5779
for _, entry := range entries {
58-
if err := Link(filepath.Join(src, entry.Name()), filepath.Join(dest, entry.Name())); err != nil {
80+
if err := Link(filepath.Join(src, entry.Name()), filepath.Join(dest, entry.Name()), visited); err != nil {
5981
return errs.Wrap(err, "sub link failed")
6082
}
6183
}

internal/smartlink/smartlink_lin_mac.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,10 @@ package smartlink
55

66
import (
77
"os"
8-
9-
"github.com/ActiveState/cli/internal/errs"
10-
"github.com/ActiveState/cli/internal/fileutils"
118
)
129

1310
// file will create a symlink from src to dest, and falls back on a hardlink if no symlink is available.
1411
// This is a workaround for the fact that Windows does not support symlinks without admin privileges.
1512
func linkFile(src, dest string) error {
16-
if fileutils.IsDir(src) {
17-
return errs.New("src is a directory, not a file: %s", src)
18-
}
1913
return os.Symlink(src, dest)
2014
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package smartlink
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"runtime"
7+
"testing"
8+
9+
"github.com/ActiveState/cli/internal/fileutils"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestLinkContentsWithCircularLink(t *testing.T) {
14+
srcDir, err := os.MkdirTemp("", "src")
15+
require.NoError(t, err)
16+
defer os.RemoveAll(srcDir)
17+
18+
destDir, err := os.MkdirTemp("", "dest")
19+
require.NoError(t, err)
20+
defer os.RemoveAll(destDir)
21+
22+
// Create test file structure:
23+
// src/
24+
// ├── regular.txt
25+
// └── subdir/
26+
// ├── circle -> subdir (circular link)
27+
// └── subfile.txt
28+
29+
testFile := filepath.Join(srcDir, "regular.txt")
30+
err = os.WriteFile(testFile, []byte("test content"), 0644)
31+
require.NoError(t, err)
32+
33+
subDir := filepath.Join(srcDir, "subdir")
34+
err = os.Mkdir(subDir, 0755)
35+
require.NoError(t, err)
36+
37+
subFile := filepath.Join(subDir, "subfile.txt")
38+
err = os.WriteFile(subFile, []byte("sub content"), 0644)
39+
require.NoError(t, err)
40+
41+
circularLink := filepath.Join(subDir, "circle")
42+
err = os.Symlink(subDir, circularLink)
43+
require.NoError(t, err)
44+
45+
err = LinkContents(srcDir, destDir, nil)
46+
if runtime.GOOS == "windows" {
47+
require.Error(t, err)
48+
return // hard links to directories are not allowed on Windows
49+
}
50+
require.NoError(t, err)
51+
52+
// Verify file structure.
53+
destFile := filepath.Join(destDir, "regular.txt")
54+
require.FileExists(t, destFile)
55+
content, err := os.ReadFile(destFile)
56+
require.NoError(t, err)
57+
require.Equal(t, "test content", string(content))
58+
59+
destSubFile := filepath.Join(destDir, "subdir", "subfile.txt")
60+
require.FileExists(t, destSubFile)
61+
subContent, err := os.ReadFile(destSubFile)
62+
require.NoError(t, err)
63+
require.Equal(t, "sub content", string(subContent))
64+
65+
destCircular := filepath.Join(destDir, "subdir", "circle")
66+
require.FileExists(t, destCircular)
67+
target, err := fileutils.ResolveUniquePath(destCircular)
68+
require.NoError(t, err)
69+
srcCircular := filepath.Join(srcDir, "subdir")
70+
if runtime.GOOS == "darwin" {
71+
srcCircular, err = fileutils.ResolveUniquePath(srcCircular) // needed for full $TMPDIR resolution
72+
require.NoError(t, err)
73+
}
74+
require.Equal(t, target, srcCircular)
75+
}

pkg/runtime/depot.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ func (d *depot) DeployViaLink(id strfmt.UUID, relativeSrc, absoluteDest string)
177177
}
178178

179179
// Copy or link the artifact files, depending on whether the artifact in question relies on file transformations
180-
if err := smartlink.LinkContents(absoluteSrc, absoluteDest); err != nil {
180+
if err := smartlink.LinkContents(absoluteSrc, absoluteDest, nil); err != nil {
181181
return errs.Wrap(err, "failed to link artifact")
182182
}
183183

@@ -295,7 +295,7 @@ func (d *depot) Undeploy(id strfmt.UUID, relativeSrc, path string) error {
295295
for sharedFile, relinkSrc := range redeploys {
296296
switch deploy.Type {
297297
case deploymentTypeLink:
298-
if err := smartlink.Link(relinkSrc, sharedFile); err != nil {
298+
if err := smartlink.Link(relinkSrc, sharedFile, nil); err != nil {
299299
return errs.Wrap(err, "failed to relink file")
300300
}
301301
case deploymentTypeCopy:

pkg/runtime/links_windows.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func supportsHardLinks(path string) (supported bool) {
4343
}
4444

4545
logging.Debug("Attempting to link '%s' to '%s'", lnk, target)
46-
err = smartlink.Link(target, lnk)
46+
err = smartlink.Link(target, lnk, nil)
4747
if err != nil {
4848
logging.Debug("Test link creation failed: %v", err)
4949
return false

0 commit comments

Comments
 (0)