Skip to content

Commit eab00b9

Browse files
committed
feat: add install-to subcommand for binary self-installation
- Add new 'install-to DIR' subcommand that copies the running binary to a specified directory - Uses atomic operations: copy to temp file, then rename for atomicity - Preserves executable permissions (755) from original binary - Handles errors gracefully with proper exit codes and cleanup - Validates destination directory exists and is actually a directory - Integrated with existing help system and command structure Implementation details: - New file: internal/cli/commands_install_to.go - core functionality - New file: internal/cli/commands_install_to_test.go - comprehensive test suite - Modified: internal/cli/main.go - CLI integration and command parsing Testing: - 7 test functions covering unit and integration testing - Tests atomic operations, error handling, permission preservation - Tests CLI interface, help system, and edge cases - All tests passing This feature enables easy binary deployment and self-updating workflows while maintaining compatibility with upstream migrate project structure.
1 parent 7efd19e commit eab00b9

File tree

3 files changed

+456
-1
lines changed

3 files changed

+456
-1
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"path/filepath"
8+
)
9+
10+
func installToCmd(destDir string) error {
11+
// Get the path to the current executable
12+
executablePath, err := os.Executable()
13+
if err != nil {
14+
return fmt.Errorf("failed to get executable path: %w", err)
15+
}
16+
17+
// Get the base name of the executable
18+
executableName := filepath.Base(executablePath)
19+
20+
// Create destination path
21+
destPath := filepath.Join(destDir, executableName)
22+
tempPath := destPath + ".tmp"
23+
24+
// Remove temp file on error
25+
defer func() {
26+
if err != nil {
27+
if _, statErr := os.Stat(tempPath); statErr == nil {
28+
os.Remove(tempPath)
29+
}
30+
}
31+
}()
32+
33+
// Get source file info to preserve permissions
34+
sourceInfo, err := os.Stat(executablePath)
35+
if err != nil {
36+
return fmt.Errorf("failed to get source file info: %w", err)
37+
}
38+
39+
// Open source file
40+
sourceFile, err := os.Open(executablePath)
41+
if err != nil {
42+
return fmt.Errorf("failed to open source file: %w", err)
43+
}
44+
defer sourceFile.Close()
45+
46+
// Create temp destination file
47+
tempFile, err := os.OpenFile(tempPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
48+
if err != nil {
49+
return fmt.Errorf("failed to create temp file: %w", err)
50+
}
51+
defer tempFile.Close()
52+
53+
// Copy the file content
54+
_, err = io.Copy(tempFile, sourceFile)
55+
if err != nil {
56+
return fmt.Errorf("failed to copy file content: %w", err)
57+
}
58+
59+
// Ensure all writes are flushed
60+
err = tempFile.Sync()
61+
if err != nil {
62+
return fmt.Errorf("failed to sync temp file: %w", err)
63+
}
64+
65+
// Close the temp file before renaming
66+
tempFile.Close()
67+
68+
// Set the correct permissions (preserve executable bit)
69+
err = os.Chmod(tempPath, sourceInfo.Mode())
70+
if err != nil {
71+
return fmt.Errorf("failed to set permissions: %w", err)
72+
}
73+
74+
// Atomically move temp file to final destination
75+
err = os.Rename(tempPath, destPath)
76+
if err != nil {
77+
return fmt.Errorf("failed to move temp file to destination: %w", err)
78+
}
79+
80+
return nil
81+
}

0 commit comments

Comments
 (0)