anomalyco/sst · error

failed to rename directory: %w

Error message

failed to rename directory: %w

What it means

After removing the old target, os.Rename(extractedDir, targetDir) puts the extracted package in its final location. If the rename fails the error is wrapped as "failed to rename directory". The most common cause is moving across filesystem boundaries (EXDEV); permissions or a re-created destination also trigger it.

Source

Thrown at pkg/runtime/python/build.go:480

		}

		// Clean up extracted directory
		if err := os.RemoveAll(extractedDir); err != nil {
			return fmt.Errorf("failed to clean up extracted directory: %w", err)
		}
	} else {
		// No src directory — check if package needs flattening
		if shouldFlattenPackage(extractedDir) {
			return flattenPackageToRoot(extractedDir, targetDir)
		}

		// Standard case: rename directory
		if err := os.RemoveAll(targetDir); err != nil {
			return fmt.Errorf("failed to remove old directory: %w", err)
		}

		if err := os.Rename(extractedDir, targetDir); err != nil {
			return fmt.Errorf("failed to rename directory: %w", err)
		}
	}

	return nil
}

// shouldFlattenPackage checks if the directory contains root-level Python files
func shouldFlattenPackage(extractedDir string) bool {
	entries, err := os.ReadDir(extractedDir)
	if err != nil {
		return false
	}

	for _, entry := range entries {
		if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".py") {
			return true
		}
	}

View on GitHub (pinned to a0bd20f762)

Solutions

  1. Place the extraction work dir on the same filesystem as targetDir
  2. Serialize builds that write the same target directory (add locking / run jobs sequentially)
  3. Verify write permission on targetDir's parent directory
  4. Fall back to copy-then-delete when rename is not possible
  5. Retry if the failure was a transient mount error

Example fix

// before
if err := os.Rename(extractedDir, targetDir); err != nil {
    return fmt.Errorf("failed to rename directory: %w", err)
}

// after: handle cross-device rename
if err := os.Rename(extractedDir, targetDir); err != nil {
    if linkErr, ok := err.(*os.LinkError); ok && linkErr.Err == syscall.EXDEV {
        if err := copyDir(extractedDir, targetDir); err != nil {
            return fmt.Errorf("failed to copy directory: %w", err)
        }
        os.RemoveAll(extractedDir)
        return nil
    }
    return fmt.Errorf("failed to rename directory: %w", err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

var s1, s2 syscall.Stat_t
if syscall.Stat(extractedDir, &s1) == nil {
    if pi, err := os.Stat(filepath.Dir(targetDir)); err == nil {
        if ps, ok := pi.Sys().(*syscall.Stat_t); ok && ps.Dev != s1.Dev {
            // different filesystems: use copy-then-delete instead of rename
        }
    }
}

Try / catch

if err := processPackageArchive(...); err != nil {
    var linkErr *os.LinkError
    if errors.As(err, &linkErr) && errors.Is(linkErr.Err, syscall.EXDEV) {
        // fall back to recursive copy
    }
    return err
}

Prevention

When it happens

Trigger: extractedDir (often under the OS temp dir) and targetDir live on different devices/mounts; targetDir reappeared between RemoveAll and Rename (concurrent build); parent dirs not writable by the build user.

Common situations: Temp on tmpfs while output is on a Docker volume; CI runners with parallel jobs installing the same package concurrently; sandboxed environments restricting rename syscalls.

Related errors


AI-assisted analysis of anomalyco/sst@a0bd20f762 (2026-08-30). Data as JSON: /api/errors/15583d0ab720eae9. Report an issue: GitHub.