Skip to content
Merged
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
4 changes: 2 additions & 2 deletions internal/archtest/baseline/no-direct-exec.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ internal/diff/compare.go:253
internal/dotfiles/dotfiles.go:23
internal/dotfiles/dotfiles.go:32
internal/dotfiles/dotfiles.go:66
internal/dotfiles/dotfiles.go:318
internal/dotfiles/dotfiles.go:416
internal/dotfiles/dotfiles.go:351
internal/dotfiles/dotfiles.go:449
internal/installer/step_system.go:85
internal/npm/npm.go:22
internal/permissions/screen_recording_cgo.go:21
Expand Down
33 changes: 33 additions & 0 deletions internal/dotfiles/dotfiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,42 @@ func linkWithMake(dotfilesPath string, dryRun bool) error {
ui.DryRunMsg("Would run make install in %s", dotfilesPath)
return nil
}

home, err := system.HomeDir()
if err != nil {
return fmt.Errorf("make install home: %w", err)
}

// Back up files that would conflict with stow-style Makefiles before
// running make install — the Makefile commonly delegates to stow, which
// aborts on pre-existing regular files.
var allBacked [][2]string
if entries, rdErr := os.ReadDir(dotfilesPath); rdErr == nil {
for _, entry := range entries {
if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
continue
}
pkgDir := filepath.Join(dotfilesPath, entry.Name())
backed, backupErr := backupConflicts(pkgDir, home, false)
if backupErr != nil {
return fmt.Errorf("backup conflicts for %s: %w", entry.Name(), backupErr)
}
allBacked = append(allBacked, backed...)
}
}

if err := system.RunCommandInDir(dotfilesPath, "make", "install"); err != nil {
for _, pair := range allBacked {
restoreFile(pair[0], pair[1], false)
}
return fmt.Errorf("make install: %w", err)
}

for _, pair := range allBacked {
if rmErr := os.Remove(pair[0]); rmErr != nil {
ui.Warn(fmt.Sprintf("could not remove backup %s: %v", pair[0], rmErr))
}
}
return nil
}

Expand Down
54 changes: 54 additions & 0 deletions internal/dotfiles/dotfiles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,60 @@ func TestLinkWithMake_DryRun(t *testing.T) {
assert.NoError(t, err)
}

func TestLinkWithMake_BacksUpConflictsBeforeRunning(t *testing.T) {
tmpHome := t.TempDir()
t.Setenv("HOME", tmpHome)

dotfilesPath := filepath.Join(tmpHome, defaultDotfilesDir)
pkgDir := filepath.Join(dotfilesPath, "git")
require.NoError(t, os.MkdirAll(pkgDir, 0755))
require.NoError(t, os.WriteFile(filepath.Join(pkgDir, ".gitconfig"), []byte("new"), 0644))

// Pre-existing file that would conflict with stow.
originalContent := "# original gitconfig\n"
require.NoError(t, os.WriteFile(filepath.Join(tmpHome, ".gitconfig"), []byte(originalContent), 0644))

// Makefile just touches a sentinel file — no real stow needed.
require.NoError(t, os.WriteFile(filepath.Join(dotfilesPath, "Makefile"),
[]byte("install:\n\ttouch \"$(HOME)/.make_ran\"\n"), 0644))

err := linkWithMake(dotfilesPath, false)
require.NoError(t, err)

// Sentinel confirms make ran.
_, statErr := os.Stat(filepath.Join(tmpHome, ".make_ran"))
assert.NoError(t, statErr, "make install should have run")

// Backup should be cleaned up after success.
_, backupErr := os.Stat(filepath.Join(tmpHome, ".gitconfig.openboot.bak"))
assert.True(t, os.IsNotExist(backupErr), "backup should be removed after success")
}

func TestLinkWithMake_RestoresConflictsOnMakeFailure(t *testing.T) {
tmpHome := t.TempDir()
t.Setenv("HOME", tmpHome)

dotfilesPath := filepath.Join(tmpHome, defaultDotfilesDir)
pkgDir := filepath.Join(dotfilesPath, "git")
require.NoError(t, os.MkdirAll(pkgDir, 0755))
require.NoError(t, os.WriteFile(filepath.Join(pkgDir, ".gitconfig"), []byte("new"), 0644))

originalContent := "# original gitconfig\n"
require.NoError(t, os.WriteFile(filepath.Join(tmpHome, ".gitconfig"), []byte(originalContent), 0644))

// Makefile that always fails.
require.NoError(t, os.WriteFile(filepath.Join(dotfilesPath, "Makefile"),
[]byte("install:\n\texit 1\n"), 0644))

err := linkWithMake(dotfilesPath, false)
assert.Error(t, err)

// Original file should be restored.
content, readErr := os.ReadFile(filepath.Join(tmpHome, ".gitconfig"))
require.NoError(t, readErr)
assert.Equal(t, originalContent, string(content), ".gitconfig should be restored after make failure")
}

func TestLink_UsesMakefileOverStow(t *testing.T) {
tmpHome := t.TempDir()
t.Setenv("HOME", tmpHome)
Expand Down
Loading