From 76276de076227b0404d7fa123ee2ebde2bdf65a6 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Thu, 4 Jun 2026 00:27:38 +0800 Subject: [PATCH] fix: back up stow conflicts before make install to prevent conflict failures linkWithMake ran make install without pre-backing-up files that stow (called by the Makefile) would conflict with. The default dotfiles repo's Makefile calls stow directly, so pre-existing .gitconfig/.zshrc on the runner caused make to exit 2, failing all three vm-e2e dotfiles tests. Apply the same backupConflicts loop that linkWithStow already uses: scan each package directory, back up regular-file conflicts, restore on make failure, clean up on success. Also updates the archtest baseline whose line numbers shifted with the added code. --- internal/archtest/baseline/no-direct-exec.txt | 4 +- internal/dotfiles/dotfiles.go | 33 ++++++++++++ internal/dotfiles/dotfiles_test.go | 54 +++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/internal/archtest/baseline/no-direct-exec.txt b/internal/archtest/baseline/no-direct-exec.txt index f4e286e..919b3b9 100644 --- a/internal/archtest/baseline/no-direct-exec.txt +++ b/internal/archtest/baseline/no-direct-exec.txt @@ -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 diff --git a/internal/dotfiles/dotfiles.go b/internal/dotfiles/dotfiles.go index 2238692..6372ebf 100644 --- a/internal/dotfiles/dotfiles.go +++ b/internal/dotfiles/dotfiles.go @@ -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 } diff --git a/internal/dotfiles/dotfiles_test.go b/internal/dotfiles/dotfiles_test.go index 1dff75c..40ad1a6 100644 --- a/internal/dotfiles/dotfiles_test.go +++ b/internal/dotfiles/dotfiles_test.go @@ -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)