From aaad6c3403d0ef468c85ced43c7a72e17a78a763 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 3 Mar 2025 22:51:52 +0000 Subject: [PATCH 1/5] feat: check for .ps1 dotfiles scripts on windows --- cli/dotfiles.go | 11 +---------- cli/dotfiles_other.go | 20 ++++++++++++++++++++ cli/dotfiles_windows.go | 12 ++++++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 cli/dotfiles_other.go create mode 100644 cli/dotfiles_windows.go diff --git a/cli/dotfiles.go b/cli/dotfiles.go index 97b323f83cfa4..0effe7efb5ecf 100644 --- a/cli/dotfiles.go +++ b/cli/dotfiles.go @@ -41,16 +41,7 @@ func (r *RootCmd) dotfiles() *serpent.Command { dotfilesDir = filepath.Join(cfgDir, dotfilesRepoDir) // This follows the same pattern outlined by others in the market: // https://github.com/coder/coder/pull/1696#issue-1245742312 - installScriptSet = []string{ - "install.sh", - "install", - "bootstrap.sh", - "bootstrap", - "script/bootstrap", - "setup.sh", - "setup", - "script/setup", - } + installScriptSet = installScriptFiles() ) if cfg == "" { diff --git a/cli/dotfiles_other.go b/cli/dotfiles_other.go new file mode 100644 index 0000000000000..6772fae480f1c --- /dev/null +++ b/cli/dotfiles_other.go @@ -0,0 +1,20 @@ +//go:build !windows + +package cli + +func installScriptFiles() []string { + return []string{ + "install.sh", + "install", + "bootstrap.sh", + "bootstrap", + "setup.sh", + "setup", + "script/install.sh", + "script/install", + "script/bootstrap.sh", + "script/bootstrap", + "script/setup.sh", + "script/setup", + } +} diff --git a/cli/dotfiles_windows.go b/cli/dotfiles_windows.go new file mode 100644 index 0000000000000..1d9f9e757b1f2 --- /dev/null +++ b/cli/dotfiles_windows.go @@ -0,0 +1,12 @@ +package cli + +func installScriptFiles() []string { + return []string{ + "install.ps1", + "bootstrap.ps1", + "setup.ps1", + "script/install.ps1", + "script/bootstrap.ps1", + "script/setup.ps1", + } +} From f0f4ef661165a185351ed19a43c17324165d8268 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 4 Mar 2025 18:22:48 +0000 Subject: [PATCH 2/5] ok but the version that actually works now --- cli/dotfiles.go | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/cli/dotfiles.go b/cli/dotfiles.go index 0effe7efb5ecf..40bf174173c09 100644 --- a/cli/dotfiles.go +++ b/cli/dotfiles.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "time" @@ -186,21 +187,28 @@ func (r *RootCmd) dotfiles() *serpent.Command { _, _ = fmt.Fprintf(inv.Stdout, "Running %s...\n", script) - // Check if the script is executable and notify on error scriptPath := filepath.Join(dotfilesDir, script) - fi, err := os.Stat(scriptPath) - if err != nil { - return xerrors.Errorf("stat %s: %w", scriptPath, err) - } - if fi.Mode()&0o111 == 0 { - return xerrors.Errorf("script %q does not have execute permissions", script) + // Permissions checks will always fail on Windows, since it doesn't have + // conventional Unix file system permissions. + if runtime.GOOS != "windows" { + // Check if the script is executable and notify on error + fi, err := os.Stat(scriptPath) + if err != nil { + return xerrors.Errorf("stat %s: %w", scriptPath, err) + } + if fi.Mode()&0o111 == 0 { + return xerrors.Errorf("script %q does not have execute permissions", script) + } } // it is safe to use a variable command here because it's from // a filtered list of pre-approved install scripts // nolint:gosec - scriptCmd := exec.CommandContext(inv.Context(), filepath.Join(dotfilesDir, script)) + scriptCmd := exec.CommandContext(inv.Context(), scriptPath) + if runtime.GOOS == "windows" { + scriptCmd = exec.CommandContext(inv.Context(), "powershell", "-NoLogo", scriptPath) + } scriptCmd.Dir = dotfilesDir scriptCmd.Stdout = inv.Stdout scriptCmd.Stderr = inv.Stderr From ee12311e80ed4a7bb5f8650f800a49eb1757b969 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 4 Mar 2025 18:44:34 +0000 Subject: [PATCH 3/5] so good at testing mhm --- cli/dotfiles_test.go | 112 ++++++++++++++++++++++++++++--------------- 1 file changed, 74 insertions(+), 38 deletions(-) diff --git a/cli/dotfiles_test.go b/cli/dotfiles_test.go index 002f001e04574..b04baf6c3a19b 100644 --- a/cli/dotfiles_test.go +++ b/cli/dotfiles_test.go @@ -116,11 +116,65 @@ func TestDotfiles(t *testing.T) { require.NoError(t, staterr) require.True(t, stat.IsDir()) }) + t.Run("SymlinkBackup", func(t *testing.T) { + t.Parallel() + _, root := clitest.New(t) + testRepo := testGitRepo(t, root) + + // nolint:gosec + err := os.WriteFile(filepath.Join(testRepo, ".bashrc"), []byte("wow"), 0o750) + require.NoError(t, err) + + // add a conflicting file at destination + // nolint:gosec + err = os.WriteFile(filepath.Join(string(root), ".bashrc"), []byte("backup"), 0o750) + require.NoError(t, err) + + c := exec.Command("git", "add", ".bashrc") + c.Dir = testRepo + err = c.Run() + require.NoError(t, err) + + c = exec.Command("git", "commit", "-m", `"add .bashrc"`) + c.Dir = testRepo + out, err := c.CombinedOutput() + require.NoError(t, err, string(out)) + + inv, _ := clitest.New(t, "dotfiles", "--global-config", string(root), "--symlink-dir", string(root), "-y", testRepo) + err = inv.Run() + require.NoError(t, err) + + b, err := os.ReadFile(filepath.Join(string(root), ".bashrc")) + require.NoError(t, err) + require.Equal(t, string(b), "wow") + + // check for backup file + b, err = os.ReadFile(filepath.Join(string(root), ".bashrc.bak")) + require.NoError(t, err) + require.Equal(t, string(b), "backup") + + // check for idempotency + inv, _ = clitest.New(t, "dotfiles", "--global-config", string(root), "--symlink-dir", string(root), "-y", testRepo) + err = inv.Run() + require.NoError(t, err) + b, err = os.ReadFile(filepath.Join(string(root), ".bashrc")) + require.NoError(t, err) + require.Equal(t, string(b), "wow") + b, err = os.ReadFile(filepath.Join(string(root), ".bashrc.bak")) + require.NoError(t, err) + require.Equal(t, string(b), "backup") + }) +} + +func TestDotfilesInstallScriptUnix(t *testing.T) { + t.Parallel() + + if runtime.GOOS == "windows" { + t.Skip() + } + t.Run("InstallScript", func(t *testing.T) { t.Parallel() - if runtime.GOOS == "windows" { - t.Skip("install scripts on windows require sh and aren't very practical") - } _, root := clitest.New(t) testRepo := testGitRepo(t, root) @@ -149,9 +203,6 @@ func TestDotfiles(t *testing.T) { t.Run("NestedInstallScript", func(t *testing.T) { t.Parallel() - if runtime.GOOS == "windows" { - t.Skip("install scripts on windows require sh and aren't very practical") - } _, root := clitest.New(t) testRepo := testGitRepo(t, root) @@ -183,9 +234,6 @@ func TestDotfiles(t *testing.T) { t.Run("InstallScriptChangeBranch", func(t *testing.T) { t.Parallel() - if runtime.GOOS == "windows" { - t.Skip("install scripts on windows require sh and aren't very practical") - } _, root := clitest.New(t) testRepo := testGitRepo(t, root) @@ -227,53 +275,41 @@ func TestDotfiles(t *testing.T) { require.NoError(t, err) require.Equal(t, string(b), "wow\n") }) - t.Run("SymlinkBackup", func(t *testing.T) { +} + +func TestDotfilesInstallScriptWindows(t *testing.T) { + t.Parallel() + + if runtime.GOOS != "windows" { + t.Skip() + } + + t.Run("InstallScript", func(t *testing.T) { t.Parallel() _, root := clitest.New(t) testRepo := testGitRepo(t, root) // nolint:gosec - err := os.WriteFile(filepath.Join(testRepo, ".bashrc"), []byte("wow"), 0o750) + err := os.WriteFile(filepath.Join(testRepo, "install.ps1"), []byte("echo \"hello, computer!\" > "+filepath.Join(string(root), "greeting.txt")), 0o750) require.NoError(t, err) - // add a conflicting file at destination - // nolint:gosec - err = os.WriteFile(filepath.Join(string(root), ".bashrc"), []byte("backup"), 0o750) - require.NoError(t, err) - - c := exec.Command("git", "add", ".bashrc") + c := exec.Command("git", "add", "install.ps1") c.Dir = testRepo err = c.Run() require.NoError(t, err) - c = exec.Command("git", "commit", "-m", `"add .bashrc"`) + c = exec.Command("git", "commit", "-m", `"add install.ps1"`) c.Dir = testRepo - out, err := c.CombinedOutput() - require.NoError(t, err, string(out)) + err = c.Run() + require.NoError(t, err) inv, _ := clitest.New(t, "dotfiles", "--global-config", string(root), "--symlink-dir", string(root), "-y", testRepo) err = inv.Run() require.NoError(t, err) - b, err := os.ReadFile(filepath.Join(string(root), ".bashrc")) - require.NoError(t, err) - require.Equal(t, string(b), "wow") - - // check for backup file - b, err = os.ReadFile(filepath.Join(string(root), ".bashrc.bak")) + b, err := os.ReadFile(filepath.Join(string(root), "greeting.txt")) require.NoError(t, err) - require.Equal(t, string(b), "backup") - - // check for idempotency - inv, _ = clitest.New(t, "dotfiles", "--global-config", string(root), "--symlink-dir", string(root), "-y", testRepo) - err = inv.Run() - require.NoError(t, err) - b, err = os.ReadFile(filepath.Join(string(root), ".bashrc")) - require.NoError(t, err) - require.Equal(t, string(b), "wow") - b, err = os.ReadFile(filepath.Join(string(root), ".bashrc.bak")) - require.NoError(t, err) - require.Equal(t, string(b), "backup") + require.Equal(t, string(b), "hello, computer!\n") }) } From e136a1abefdf38e179677a4c2151ac703b7b945b Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 4 Mar 2025 19:16:32 +0000 Subject: [PATCH 4/5] *shakes fist at windows* --- cli/dotfiles_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/dotfiles_test.go b/cli/dotfiles_test.go index b04baf6c3a19b..b4662de0f3c2d 100644 --- a/cli/dotfiles_test.go +++ b/cli/dotfiles_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "runtime" "testing" + "unicode/utf16" "github.com/stretchr/testify/require" @@ -309,7 +310,7 @@ func TestDotfilesInstallScriptWindows(t *testing.T) { b, err := os.ReadFile(filepath.Join(string(root), "greeting.txt")) require.NoError(t, err) - require.Equal(t, string(b), "hello, computer!\n") + require.Equal(t, string(b), utf16.Encode([]rune("hello, computer!\n"))) }) } From 304260cac810078006b1af2ca6b2464ba78f33c8 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 4 Mar 2025 19:49:50 +0000 Subject: [PATCH 5/5] classic windows, what a normal operating system --- cli/dotfiles_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cli/dotfiles_test.go b/cli/dotfiles_test.go index b4662de0f3c2d..32169f9e98c65 100644 --- a/cli/dotfiles_test.go +++ b/cli/dotfiles_test.go @@ -7,7 +7,6 @@ import ( "path/filepath" "runtime" "testing" - "unicode/utf16" "github.com/stretchr/testify/require" @@ -310,7 +309,9 @@ func TestDotfilesInstallScriptWindows(t *testing.T) { b, err := os.ReadFile(filepath.Join(string(root), "greeting.txt")) require.NoError(t, err) - require.Equal(t, string(b), utf16.Encode([]rune("hello, computer!\n"))) + // If you squint, it does in fact say "hello, computer!" in here, but in + // UTF-16 and with a byte-order-marker at the beginning. Windows! + require.Equal(t, b, []byte("\xff\xfeh\x00e\x00l\x00l\x00o\x00,\x00 \x00c\x00o\x00m\x00p\x00u\x00t\x00e\x00r\x00!\x00\r\x00\n\x00")) }) }