diff --git a/internal/archtest/baseline/dryrun.txt b/internal/archtest/baseline/dryrun.txt index 6d663e2..146493f 100644 --- a/internal/archtest/baseline/dryrun.txt +++ b/internal/archtest/baseline/dryrun.txt @@ -1,7 +1,5 @@ # Baseline for archtest rule "dryrun". -# Each line is : # reason for a known existing violation. +# Each line is : of a known existing violation. # Regenerate: ARCHTEST_UPDATE_BASELINE=1 go test ./internal/archtest/... - -# Audited exemptions: read-only diagnostic runners — doctor never mutates user state. -internal/doctor/doctor.go:50 # read-only diagnostic probe; doctor does not mutate user state -internal/doctor/doctor.go:54 # read-only diagnostic probe; doctor does not mutate user state +internal/doctor/doctor.go:50 +internal/doctor/doctor.go:54 diff --git a/internal/archtest/baseline/no-direct-exec.txt b/internal/archtest/baseline/no-direct-exec.txt index 0f85b6b..f4e286e 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:290 -internal/dotfiles/dotfiles.go:388 +internal/dotfiles/dotfiles.go:318 +internal/dotfiles/dotfiles.go:416 internal/installer/step_system.go:85 internal/npm/npm.go:22 internal/permissions/screen_recording_cgo.go:21 diff --git a/internal/archtest/baseline/no-raw-http.txt b/internal/archtest/baseline/no-raw-http.txt index 581de79..14bc723 100644 --- a/internal/archtest/baseline/no-raw-http.txt +++ b/internal/archtest/baseline/no-raw-http.txt @@ -1,5 +1,5 @@ # Baseline for archtest rule "no-raw-http". -# Each line is : # reason for a known existing violation. +# Each line is : of a known existing violation. # Regenerate: ARCHTEST_UPDATE_BASELINE=1 go test ./internal/archtest/... -internal/config/packages_remote.go:84 # injects the default transport into a test-swappable client; round-trip uses httputil.Do -internal/config/remote.go:57 # injects the default transport into a version-header transport; round-trip uses httputil.Do +internal/config/packages_remote.go:84 +internal/config/remote.go:57 diff --git a/internal/dotfiles/dotfiles.go b/internal/dotfiles/dotfiles.go index 2967572..2238692 100644 --- a/internal/dotfiles/dotfiles.go +++ b/internal/dotfiles/dotfiles.go @@ -225,6 +225,10 @@ func Link(dryRun bool) error { return fmt.Errorf("dotfiles directory not found: %s", dotfilesPath) } + if hasMakefile(dotfilesPath) { + return linkWithMake(dotfilesPath, dryRun) + } + if hasStowPackages(dotfilesPath) { return linkWithStow(dotfilesPath, dryRun) } @@ -232,6 +236,30 @@ func Link(dryRun bool) error { return linkDirect(dotfilesPath, dryRun) } +func hasMakefile(dotfilesPath string) bool { + data, err := os.ReadFile(filepath.Join(dotfilesPath, "Makefile")) + if err != nil { + return false + } + for _, line := range strings.Split(string(data), "\n") { + if strings.HasPrefix(line, "install:") { + return true + } + } + return false +} + +func linkWithMake(dotfilesPath string, dryRun bool) error { + if dryRun { + ui.DryRunMsg("Would run make install in %s", dotfilesPath) + return nil + } + if err := system.RunCommandInDir(dotfilesPath, "make", "install"); err != nil { + return fmt.Errorf("make install: %w", err) + } + return nil +} + func hasStowPackages(dotfilesPath string) bool { entries, err := os.ReadDir(dotfilesPath) if err != nil { diff --git a/internal/dotfiles/dotfiles_test.go b/internal/dotfiles/dotfiles_test.go index 2865d3f..1dff75c 100644 --- a/internal/dotfiles/dotfiles_test.go +++ b/internal/dotfiles/dotfiles_test.go @@ -122,6 +122,54 @@ func TestLink_DryRun(t *testing.T) { assert.True(t, os.IsNotExist(err)) } +func TestHasMakefile_WithInstallTarget(t *testing.T) { + tmpDir := t.TempDir() + err := os.WriteFile(filepath.Join(tmpDir, "Makefile"), []byte("install:\n\techo done\n"), 0644) + require.NoError(t, err) + assert.True(t, hasMakefile(tmpDir)) +} + +func TestHasMakefile_WithoutInstallTarget(t *testing.T) { + tmpDir := t.TempDir() + err := os.WriteFile(filepath.Join(tmpDir, "Makefile"), []byte("build:\n\techo build\n"), 0644) + require.NoError(t, err) + assert.False(t, hasMakefile(tmpDir)) +} + +func TestHasMakefile_NoMakefile(t *testing.T) { + tmpDir := t.TempDir() + assert.False(t, hasMakefile(tmpDir)) +} + +func TestLinkWithMake_DryRun(t *testing.T) { + tmpHome := t.TempDir() + t.Setenv("HOME", tmpHome) + + dotfilesPath := filepath.Join(tmpHome, defaultDotfilesDir) + require.NoError(t, os.MkdirAll(dotfilesPath, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(dotfilesPath, "Makefile"), []byte("install:\n\techo done\n"), 0644)) + + err := linkWithMake(dotfilesPath, true) + assert.NoError(t, err) +} + +func TestLink_UsesMakefileOverStow(t *testing.T) { + tmpHome := t.TempDir() + t.Setenv("HOME", tmpHome) + + dotfilesPath := filepath.Join(tmpHome, defaultDotfilesDir) + require.NoError(t, os.MkdirAll(dotfilesPath, 0755)) + + // Repo has both a Makefile and a stow package — Makefile wins (dry run). + require.NoError(t, os.WriteFile(filepath.Join(dotfilesPath, "Makefile"), []byte("install:\n\techo done\n"), 0644)) + pkgDir := filepath.Join(dotfilesPath, "vim") + require.NoError(t, os.MkdirAll(pkgDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(pkgDir, ".vimrc"), []byte(""), 0644)) + + err := Link(true) + assert.NoError(t, err) +} + func TestHasStowPackages_Empty(t *testing.T) { tmpDir := t.TempDir() result := hasStowPackages(tmpDir) diff --git a/internal/system/system.go b/internal/system/system.go index 36abe98..902945d 100644 --- a/internal/system/system.go +++ b/internal/system/system.go @@ -28,6 +28,16 @@ func RunCommandContext(ctx context.Context, name string, args ...string) error { return cmd.Run() } +// RunCommandInDir runs name with args in the given working directory, +// forwarding stdout and stderr to the terminal. +func RunCommandInDir(dir string, name string, args ...string) error { + cmd := exec.CommandContext(context.Background(), name, args...) //nolint:gosec // intentional generic runner; callers are responsible for validating name and args + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + func RunCommandSilent(name string, args ...string) (string, error) { return RunCommandSilentContext(context.Background(), name, args...) }