diff --git a/cli/dotfiles.go b/cli/dotfiles.go index 97b323f83c..40bf174173 100644 --- a/cli/dotfiles.go +++ b/cli/dotfiles.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "time" @@ -41,16 +42,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 == "" { @@ -195,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 diff --git a/cli/dotfiles_other.go b/cli/dotfiles_other.go new file mode 100644 index 0000000000..6772fae480 --- /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_test.go b/cli/dotfiles_test.go index 002f001e04..32169f9e98 100644 --- a/cli/dotfiles_test.go +++ b/cli/dotfiles_test.go @@ -116,117 +116,6 @@ func TestDotfiles(t *testing.T) { require.NoError(t, staterr) require.True(t, stat.IsDir()) }) - 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) - - // nolint:gosec - err := os.WriteFile(filepath.Join(testRepo, "install.sh"), []byte("#!/bin/bash\necho wow > "+filepath.Join(string(root), ".bashrc")), 0o750) - require.NoError(t, err) - - c := exec.Command("git", "add", "install.sh") - c.Dir = testRepo - err = c.Run() - require.NoError(t, err) - - c = exec.Command("git", "commit", "-m", `"add install.sh"`) - c.Dir = testRepo - 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\n") - }) - - 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) - - scriptPath := filepath.Join("script", "setup") - err := os.MkdirAll(filepath.Join(testRepo, "script"), 0o750) - require.NoError(t, err) - // nolint:gosec - err = os.WriteFile(filepath.Join(testRepo, scriptPath), []byte("#!/bin/bash\necho wow > "+filepath.Join(string(root), ".bashrc")), 0o750) - require.NoError(t, err) - - c := exec.Command("git", "add", scriptPath) - c.Dir = testRepo - err = c.Run() - require.NoError(t, err) - - c = exec.Command("git", "commit", "-m", `"add script"`) - c.Dir = testRepo - 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\n") - }) - - 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) - - // We need an initial commit to start the `main` branch - c := exec.Command("git", "commit", "--allow-empty", "-m", `"initial commit"`) - c.Dir = testRepo - err := c.Run() - require.NoError(t, err) - - // nolint:gosec - err = os.WriteFile(filepath.Join(testRepo, "install.sh"), []byte("#!/bin/bash\necho wow > "+filepath.Join(string(root), ".bashrc")), 0o750) - require.NoError(t, err) - - c = exec.Command("git", "checkout", "-b", "other_branch") - c.Dir = testRepo - err = c.Run() - require.NoError(t, err) - - c = exec.Command("git", "add", "install.sh") - c.Dir = testRepo - err = c.Run() - require.NoError(t, err) - - c = exec.Command("git", "commit", "-m", `"add install.sh"`) - c.Dir = testRepo - err = c.Run() - require.NoError(t, err) - - c = exec.Command("git", "checkout", "main") - c.Dir = testRepo - err = c.Run() - require.NoError(t, err) - - inv, _ := clitest.New(t, "dotfiles", "--global-config", string(root), "--symlink-dir", string(root), "-y", testRepo, "-b", "other_branch") - 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\n") - }) t.Run("SymlinkBackup", func(t *testing.T) { t.Parallel() _, root := clitest.New(t) @@ -277,6 +166,155 @@ func TestDotfiles(t *testing.T) { }) } +func TestDotfilesInstallScriptUnix(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, "install.sh"), []byte("#!/bin/bash\necho wow > "+filepath.Join(string(root), ".bashrc")), 0o750) + require.NoError(t, err) + + c := exec.Command("git", "add", "install.sh") + c.Dir = testRepo + err = c.Run() + require.NoError(t, err) + + c = exec.Command("git", "commit", "-m", `"add install.sh"`) + c.Dir = testRepo + 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\n") + }) + + t.Run("NestedInstallScript", func(t *testing.T) { + t.Parallel() + _, root := clitest.New(t) + testRepo := testGitRepo(t, root) + + scriptPath := filepath.Join("script", "setup") + err := os.MkdirAll(filepath.Join(testRepo, "script"), 0o750) + require.NoError(t, err) + // nolint:gosec + err = os.WriteFile(filepath.Join(testRepo, scriptPath), []byte("#!/bin/bash\necho wow > "+filepath.Join(string(root), ".bashrc")), 0o750) + require.NoError(t, err) + + c := exec.Command("git", "add", scriptPath) + c.Dir = testRepo + err = c.Run() + require.NoError(t, err) + + c = exec.Command("git", "commit", "-m", `"add script"`) + c.Dir = testRepo + 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\n") + }) + + t.Run("InstallScriptChangeBranch", func(t *testing.T) { + t.Parallel() + _, root := clitest.New(t) + testRepo := testGitRepo(t, root) + + // We need an initial commit to start the `main` branch + c := exec.Command("git", "commit", "--allow-empty", "-m", `"initial commit"`) + c.Dir = testRepo + err := c.Run() + require.NoError(t, err) + + // nolint:gosec + err = os.WriteFile(filepath.Join(testRepo, "install.sh"), []byte("#!/bin/bash\necho wow > "+filepath.Join(string(root), ".bashrc")), 0o750) + require.NoError(t, err) + + c = exec.Command("git", "checkout", "-b", "other_branch") + c.Dir = testRepo + err = c.Run() + require.NoError(t, err) + + c = exec.Command("git", "add", "install.sh") + c.Dir = testRepo + err = c.Run() + require.NoError(t, err) + + c = exec.Command("git", "commit", "-m", `"add install.sh"`) + c.Dir = testRepo + err = c.Run() + require.NoError(t, err) + + c = exec.Command("git", "checkout", "main") + c.Dir = testRepo + err = c.Run() + require.NoError(t, err) + + inv, _ := clitest.New(t, "dotfiles", "--global-config", string(root), "--symlink-dir", string(root), "-y", testRepo, "-b", "other_branch") + 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\n") + }) +} + +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, "install.ps1"), []byte("echo \"hello, computer!\" > "+filepath.Join(string(root), "greeting.txt")), 0o750) + require.NoError(t, err) + + c := exec.Command("git", "add", "install.ps1") + c.Dir = testRepo + err = c.Run() + require.NoError(t, err) + + c = exec.Command("git", "commit", "-m", `"add install.ps1"`) + c.Dir = testRepo + 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), "greeting.txt")) + require.NoError(t, err) + // 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")) + }) +} + func testGitRepo(t *testing.T, root config.Root) string { r, err := cryptorand.String(8) require.NoError(t, err) diff --git a/cli/dotfiles_windows.go b/cli/dotfiles_windows.go new file mode 100644 index 0000000000..1d9f9e757b --- /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", + } +}