Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/internal/config/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Paths struct {
Versions string // Versions directory (~/.dtvem/versions)
Config string // Config directory (~/.dtvem/config)
Cache string // Cache directory (~/.dtvem/cache)
Temp string // Staging directory for in-progress installs (~/.dtvem/tmp)
}

var (
Expand All @@ -42,6 +43,7 @@ func initPaths() *Paths {
Versions: filepath.Join(root, "versions"),
Config: filepath.Join(root, "config"),
Cache: filepath.Join(root, "cache"),
Temp: filepath.Join(root, "tmp"),
}
}

Expand Down Expand Up @@ -111,6 +113,31 @@ func RuntimeVersionPath(runtimeName, version string) string {
return filepath.Join(paths.Versions, runtimeName, version)
}

// StagingPath returns the scratch directory used to download and extract a
// runtime version before it is moved into the versions directory.
//
// It lives under the dtvem root rather than the OS temp directory so the final
// move is a same-volume rename: os.Rename cannot move a directory across drives
// on Windows, or across filesystems on Unix (e.g. a tmpfs-backed /tmp).
func StagingPath(runtimeName, version string) string {
paths := DefaultPaths()
return filepath.Join(paths.Temp, runtimeName+"-"+version)
}

// CreateStagingDir creates an empty staging directory for a runtime version
// and returns its path. Leftovers from an earlier interrupted install are
// removed first, since nothing else cleans up the dtvem root.
func CreateStagingDir(runtimeName, version string) (string, error) {
dir := StagingPath(runtimeName, version)
if err := os.RemoveAll(dir); err != nil {
return "", err
}
if err := os.MkdirAll(dir, 0755); err != nil {
return "", err
}
return dir, nil
}

// GlobalConfigPath returns the path to the global config file
func GlobalConfigPath() string {
paths := DefaultPaths()
Expand Down
90 changes: 90 additions & 0 deletions src/internal/config/paths_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,96 @@ func TestGetPaths(t *testing.T) {
if !strings.HasPrefix(paths.Config, paths.Root) {
t.Errorf("Config path %q should be under Root %q", paths.Config, paths.Root)
}
if !strings.HasPrefix(paths.Temp, paths.Root) {
t.Errorf("Temp path %q should be under Root %q", paths.Temp, paths.Root)
}
}

// useTempRoot points DTVEM_ROOT at a fresh temp directory for the duration of
// the test and returns it.
func useTempRoot(t *testing.T) string {
t.Helper()
root := t.TempDir()
t.Setenv("DTVEM_ROOT", root)
resetPathsForTesting()
t.Cleanup(resetPathsForTesting)
return root
}

func TestStagingPath(t *testing.T) {
root := useTempRoot(t)

got := StagingPath("node", "24.22.0")
want := filepath.Join(root, "tmp", "node-24.22.0")
if got != want {
t.Errorf("StagingPath(%q, %q) = %q, want %q", "node", "24.22.0", got, want)
}
}

func TestStagingPath_SameVolumeAsVersions(t *testing.T) {
// Installs finish with os.Rename from the staging path to the version
// path, which fails across drives on Windows (issue #278). Both must
// resolve under the dtvem root, never the OS temp directory.
root := useTempRoot(t)

staging := StagingPath("python", "3.12.0")
install := RuntimeVersionPath("python", "3.12.0")

if filepath.VolumeName(staging) != filepath.VolumeName(install) {
t.Errorf("StagingPath volume %q differs from RuntimeVersionPath volume %q",
filepath.VolumeName(staging), filepath.VolumeName(install))
}
if !strings.HasPrefix(staging, root) {
t.Errorf("StagingPath() = %q, should be under root %q", staging, root)
}
if strings.HasPrefix(staging, DefaultPaths().Versions) {
t.Errorf("StagingPath() = %q, should not be under the versions directory", staging)
}
}

func TestCreateStagingDir(t *testing.T) {
useTempRoot(t)

dir, err := CreateStagingDir("ruby", "3.3.0")
if err != nil {
t.Fatalf("CreateStagingDir() error: %v", err)
}
if want := StagingPath("ruby", "3.3.0"); dir != want {
t.Errorf("CreateStagingDir() = %q, want %q", dir, want)
}

info, err := os.Stat(dir)
if err != nil {
t.Fatalf("staging directory was not created: %v", err)
}
if !info.IsDir() {
t.Errorf("staging path %q is not a directory", dir)
}
}

func TestCreateStagingDir_RemovesLeftovers(t *testing.T) {
useTempRoot(t)

stale := filepath.Join(StagingPath("node", "22.0.0"), "extracted", "node.exe")
if err := os.MkdirAll(filepath.Dir(stale), 0755); err != nil {
t.Fatalf("failed to create stale directory: %v", err)
}
if err := os.WriteFile(stale, []byte("stale"), 0644); err != nil {
t.Fatalf("failed to create stale file: %v", err)
}

dir, err := CreateStagingDir("node", "22.0.0")
if err != nil {
t.Fatalf("CreateStagingDir() error: %v", err)
}

entries, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("failed to read staging directory: %v", err)
}
if len(entries) != 0 {
t.Errorf("CreateStagingDir() left %d stale entries in %q", len(entries), dir)
}
}

func TestRuntimeVersionPath(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions src/runtimes/node/provider_full.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ func (p *Provider) Install(version string) error {

ui.Progress("Downloading from %s", downloadURL)

tempDir := filepath.Join(os.TempDir(), fmt.Sprintf("dtvem-node-%s", version))
if err := os.MkdirAll(tempDir, 0755); err != nil {
tempDir, err := config.CreateStagingDir("node", version)
if err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer func() { _ = os.RemoveAll(tempDir) }()
Expand Down
4 changes: 2 additions & 2 deletions src/runtimes/python/provider_full.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import (
func (p *Provider) downloadAndExtract(version, downloadURL, archiveName string) (extractDir string, cleanup func(), err error) {
ui.Progress("Downloading from %s", downloadURL)

tempDir := filepath.Join(os.TempDir(), fmt.Sprintf("dtvem-python-%s", version))
if err := os.MkdirAll(tempDir, 0755); err != nil {
tempDir, err := config.CreateStagingDir("python", version)
if err != nil {
return "", nil, fmt.Errorf("failed to create temp directory: %w", err)
}

Expand Down
4 changes: 2 additions & 2 deletions src/runtimes/ruby/provider_full.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ func (p *Provider) Install(version string) error {
func (p *Provider) downloadAndExtract(version, downloadURL, archiveName string) (extractDir string, cleanup func(), err error) {
ui.Progress("Downloading from %s", downloadURL)

tempDir := filepath.Join(os.TempDir(), fmt.Sprintf("dtvem-ruby-%s", version))
if err := os.MkdirAll(tempDir, 0755); err != nil {
tempDir, err := config.CreateStagingDir("ruby", version)
if err != nil {
return "", nil, fmt.Errorf("failed to create temp directory: %w", err)
}

Expand Down
Loading