Skip to content

Commit

Permalink
Merge pull request #3600 from ActiveState/mitchell/dx-3167-2
Browse files Browse the repository at this point in the history
Handle recursive links in runtime sources.
  • Loading branch information
mitchell-as authored Nov 27, 2024
2 parents fbf7ca2 + d1a7e18 commit 6054081
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 6 deletions.
12 changes: 12 additions & 0 deletions internal/smartlink/smartlink.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,25 @@ func LinkContents(src, dest string) error {
// Link creates a link from src to target. MS decided to support Symlinks but only if you opt into developer mode (go figure),
// which we cannot reasonably force on our users. So on Windows we will instead create dirs and hardlinks.
func Link(src, dest string) error {
originalSrc := src

var err error
src, dest, err = resolvePaths(src, dest)
if err != nil {
return errs.Wrap(err, "Could not resolve src and dest paths")
}

if fileutils.IsDir(src) {
if fileutils.IsSymlink(originalSrc) {
// If the original src is a symlink, the resolved src is no longer a symlink and could point
// to a parent directory, resulting in a recursive directory structure.
// Avoid any potential problems by simply linking the original link to the target.
// Links to directories are okay on Linux and macOS, but will fail on Windows.
// If we ever get here on Windows, the artifact being deployed is bad and there's nothing we
// can do about it except receive the report from Rollbar and report it internally.
return linkFile(originalSrc, dest)
}

if err := fileutils.Mkdir(dest); err != nil {
return errs.Wrap(err, "could not create directory %s", dest)
}
Expand Down
6 changes: 0 additions & 6 deletions internal/smartlink/smartlink_lin_mac.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,10 @@ package smartlink

import (
"os"

"github.com/ActiveState/cli/internal/errs"
"github.com/ActiveState/cli/internal/fileutils"
)

// file will create a symlink from src to dest, and falls back on a hardlink if no symlink is available.
// This is a workaround for the fact that Windows does not support symlinks without admin privileges.
func linkFile(src, dest string) error {
if fileutils.IsDir(src) {
return errs.New("src is a directory, not a file: %s", src)
}
return os.Symlink(src, dest)
}
75 changes: 75 additions & 0 deletions internal/smartlink/smartlink_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package smartlink

import (
"os"
"path/filepath"
"runtime"
"testing"

"github.com/ActiveState/cli/internal/fileutils"
"github.com/stretchr/testify/require"
)

func TestLinkContentsWithCircularLink(t *testing.T) {
srcDir, err := os.MkdirTemp("", "src")
require.NoError(t, err)
defer os.RemoveAll(srcDir)

destDir, err := os.MkdirTemp("", "dest")
require.NoError(t, err)
defer os.RemoveAll(destDir)

// Create test file structure:
// src/
// ├── regular.txt
// └── subdir/
// ├── circle -> subdir (circular link)
// └── subfile.txt

testFile := filepath.Join(srcDir, "regular.txt")
err = os.WriteFile(testFile, []byte("test content"), 0644)
require.NoError(t, err)

subDir := filepath.Join(srcDir, "subdir")
err = os.Mkdir(subDir, 0755)
require.NoError(t, err)

subFile := filepath.Join(subDir, "subfile.txt")
err = os.WriteFile(subFile, []byte("sub content"), 0644)
require.NoError(t, err)

circularLink := filepath.Join(subDir, "circle")
err = os.Symlink(subDir, circularLink)
require.NoError(t, err)

err = LinkContents(srcDir, destDir)
if runtime.GOOS == "windows" {
require.Error(t, err)
return // hard links to directories are not allowed on Windows
}
require.NoError(t, err)

// Verify file structure.
destFile := filepath.Join(destDir, "regular.txt")
require.FileExists(t, destFile)
content, err := os.ReadFile(destFile)
require.NoError(t, err)
require.Equal(t, "test content", string(content))

destSubFile := filepath.Join(destDir, "subdir", "subfile.txt")
require.FileExists(t, destSubFile)
subContent, err := os.ReadFile(destSubFile)
require.NoError(t, err)
require.Equal(t, "sub content", string(subContent))

destCircular := filepath.Join(destDir, "subdir", "circle")
require.FileExists(t, destCircular)
target, err := fileutils.ResolveUniquePath(destCircular)
require.NoError(t, err)
srcCircular := filepath.Join(srcDir, "subdir")
if runtime.GOOS == "darwin" {
srcCircular, err = fileutils.ResolveUniquePath(srcCircular) // needed for full $TMPDIR resolution
require.NoError(t, err)
}
require.Equal(t, target, srcCircular)
}

0 comments on commit 6054081

Please sign in to comment.