Repository: bazelbuild/bazelisk
Pull request #417 (2023-01-23 17:47:28)

Add support for `tools/bazel.ps1` and `tools/bazel.bat` on Windows
closes https://github.com/bazelbuild/bazelisk/issues/416

google-cla[bot]: Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this [failed invocation](https://github.com/bazelbuild/bazelisk/pull/417/checks?check_run_id=10829164874) of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.
UebelAndre: @philwo @fweikert hey, would either of you be able to take a look at this PR? 😅 
fweikert: Nice, thanks for this PR (and especially for adding a test!). I'll reserve some time tomorrow to work this PR and the others in our backlog.

Patch:
diff --git a/.bazelversion b/.bazelversion
index 03f488b..09b254e 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1,1 +1,1 @@
-5.3.0
+6.0.0
diff --git a/core/BUILD b/core/BUILD
index fee6d3a..2f5ec60 100644
--- a/core/BUILD
+++ b/core/BUILD
@@ -1,4 +1,4 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
 
 go_library(
     name = "go_default_library",
@@ -16,3 +16,9 @@ go_library(
         "@com_github_mitchellh_go_homedir//:go_default_library",
     ],
 )
+
+go_test(
+    name = "go_default_test",
+    srcs = ["core_test.go"],
+    embed = [":go_default_library"],
+)
diff --git a/core/core.go b/core/core.go
index 0fa1f7e..01f5f68 100644
--- a/core/core.go
+++ b/core/core.go
@@ -5,6 +5,7 @@ package core
 
 import (
 	"bufio"
+	"crypto/sha256"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -14,6 +15,7 @@ import (
 	"os/signal"
 	"path/filepath"
 	"regexp"
+	"runtime"
 	"sort"
 	"strings"
 	"sync"
@@ -30,6 +32,7 @@ const (
 	skipWrapperEnv = "BAZELISK_SKIP_WRAPPER"
 	wrapperPath    = "./tools/bazel"
 	rcFileName     = ".bazeliskrc"
+	maxDirLength   = 255
 )
 
 var (
@@ -456,23 +459,39 @@ func linkLocalBazel(baseDirectory string, bazelPath string) (string, error) {
 	return destinationPath, nil
 }
 
-func maybeDelegateToWrapper(bazel string) string {
-	if GetEnvOrConfig(skipWrapperEnv) != "" {
-		return bazel
-	}
-
-	wd, err := os.Getwd()
-	if err != nil {
+func maybeDelegateToWrapperFromDir(bazel string, wd string, ignoreEnv bool) string {
+	if !ignoreEnv && GetEnvOrConfig(skipWrapperEnv) != "" {
 		return bazel
 	}
 
 	root := findWorkspaceRoot(wd)
 	wrapper := filepath.Join(root, wrapperPath)
-	if stat, err := os.Stat(wrapper); err != nil || stat.IsDir() || stat.Mode().Perm()&0111 == 0 {
+	if stat, err := os.Stat(wrapper); err == nil && !stat.Mode().IsDir() && stat.Mode().Perm()&0111 != 0 {
+		return wrapper
+	}
+
+	if runtime.GOOS == "windows" {
+		powershellWrapper := filepath.Join(root, wrapperPath + ".ps1")
+		if stat, err := os.Stat(powershellWrapper); err == nil && !stat.Mode().IsDir() {
+			return powershellWrapper
+		}
+
+		batchWrapper := filepath.Join(root, wrapperPath + ".bat")
+		if stat, err := os.Stat(batchWrapper); err == nil && !stat.Mode().IsDir() {
+			return batchWrapper
+		}
+	}
+
+	return bazel
+}
+
+func maybeDelegateToWrapper(bazel string) string {
+	wd, err := os.Getwd()
+	if err != nil {
 		return bazel
 	}
 
-	return wrapper
+	return maybeDelegateToWrapperFromDir(bazel, wd, false)
 }
 
 func prependDirToPathList(cmd *exec.Cmd, dir string) {
@@ -729,5 +748,12 @@ func migrate(bazelPath string, baseArgs []string, flags []string) {
 
 func dirForURL(url string) string {
 	// Replace all characters that might not be allowed in filenames with "-".
-	return regexp.MustCompile("[[:^alnum:]]").ReplaceAllString(url, "-")
+	dir := regexp.MustCompile("[[:^alnum:]]").ReplaceAllString(url, "-")
+	// Work around length limit on some systems by truncating and then appending
+	// a sha256 hash of the URL.
+	if len(dir) > maxDirLength {
+		suffix := fmt.Sprintf("...%x", sha256.Sum256([]byte(url)))
+		dir = dir[:maxDirLength-len(suffix)] + suffix
+	}
+	return dir
 }
diff --git a/go.mod b/go.mod
index e18a539..fb48e45 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/bazelbuild/bazelisk
 go 1.18
 
 require (
-	github.com/bazelbuild/rules_go v0.37.0
+	github.com/bazelbuild/rules_go v0.38.1
 	github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d
 	github.com/hashicorp/go-version v1.6.0
 	github.com/mitchellh/go-homedir v1.1.0
diff --git a/go.sum b/go.sum
index dc4a87a..bf171a5 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-github.com/bazelbuild/rules_go v0.37.0 h1:vbnESGv/t2WgGEbXatwbXAS95dTx93Lv6Uh5QkVF13s=
-github.com/bazelbuild/rules_go v0.37.0/go.mod h1:TMHmtfpvyfsxaqfL9WnahCsXMWDMICTw7XeK9yVb+YU=
+github.com/bazelbuild/rules_go v0.38.1 h1:YGNsLhWe18Ielebav7cClP3GMwBxBE+xEArLHtmXDx8=
+github.com/bazelbuild/rules_go v0.38.1/go.mod h1:TMHmtfpvyfsxaqfL9WnahCsXMWDMICTw7XeK9yVb+YU=
 github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
 github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
 github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
diff --git a/repositories/github.go b/repositories/github.go
index f03fd6a..0d02067 100644
--- a/repositories/github.go
+++ b/repositories/github.go
@@ -52,7 +52,11 @@ func (gh *GitHubRepo) getFilteredVersions(bazeliskHome, bazelFork string, wantPr
 	}
 
 	url := fmt.Sprintf("https://api.github.com/repos/%s/bazel/releases", bazelFork)
-	releasesJSON, err := httputil.MaybeDownload(bazeliskHome, url, bazelFork+"-releases.json", "list of Bazel releases from github.com/"+bazelFork, fmt.Sprintf("token %s", gh.token), merger)
+	auth := ""
+	if gh.token != "" {
+		auth = fmt.Sprintf("token %s", gh.token)
+	}
+	releasesJSON, err := httputil.MaybeDownload(bazeliskHome, url, bazelFork+"-releases.json", "list of Bazel releases from github.com/"+bazelFork, auth, merger)
 	if err != nil {
 		return []string{}, fmt.Errorf("unable to determine '%s' releases: %v", bazelFork, err)
 	}


Test patch:
diff --git a/core/core_test.go b/core/core_test.go
new file mode 100644
index 0000000..9f1dea8
--- /dev/null
+++ b/core/core_test.go
@@ -0,0 +1,169 @@
+package core
+
+import (
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+	"runtime"
+	"testing"
+)
+
+func TestMaybeDelegateToNoWrapper(t *testing.T) {
+	tmpDir, err := ioutil.TempDir("", "TestMaybeDelegateToNoWrapper")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer os.RemoveAll(tmpDir)
+
+	os.MkdirAll(tmpDir, os.ModeDir | 0700)
+	ioutil.WriteFile(filepath.Join(tmpDir, "WORKSPACE"), []byte(""), 0600)
+	ioutil.WriteFile(filepath.Join(tmpDir, "BUILD"), []byte(""), 0600)
+
+	entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, true)
+	expected := "bazel_real"
+
+	if entrypoint != expected {
+		t.Fatalf("Expected to delegate bazel to %q, but got %q", expected, entrypoint)
+	}
+}
+
+func TestMaybeDelegateToNoNonExecutableWrapper(t *testing.T) {
+	// It's not guaranteed that `tools/bazel` is executable on the
+	// Windows host running this test. Thus the test is skipped on
+	// this platform to guarantee consistent results.
+	if runtime.GOOS == "windows" {
+		return
+	}
+	
+	tmpDir, err := ioutil.TempDir("", "TestMaybeDelegateToNoNonExecutableWrapper")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer os.RemoveAll(tmpDir)
+
+	os.MkdirAll(tmpDir, os.ModeDir | 0700)
+	ioutil.WriteFile(filepath.Join(tmpDir, "WORKSPACE"), []byte(""), 0600)
+	ioutil.WriteFile(filepath.Join(tmpDir, "BUILD"), []byte(""), 0600)
+
+	os.MkdirAll(filepath.Join(tmpDir, "tools"), os.ModeDir | 0700)
+	ioutil.WriteFile(filepath.Join(tmpDir, "tools", "bazel"), []byte(""), 0600)
+
+	entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, true)
+	expected := "bazel_real"
+
+	if entrypoint != expected {
+		t.Fatalf("Expected to delegate bazel to %q, but got %q", expected, entrypoint)
+	}
+}
+
+func TestMaybeDelegateToStandardWrapper(t *testing.T) {
+	// It's not guaranteed that `tools/bazel` is executable on the
+	// Windows host running this test. Thus the test is skipped on
+	// this platform to guarantee consistent results.
+	if runtime.GOOS == "windows" {
+		return
+	}
+
+	var tmpDir, err = ioutil.TempDir("", "TestMaybeDelegateToStandardWrapper")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer os.RemoveAll(tmpDir)
+
+	os.MkdirAll(tmpDir, os.ModeDir | 0700)
+	ioutil.WriteFile(filepath.Join(tmpDir, "WORKSPACE"), []byte(""), 0600)
+	ioutil.WriteFile(filepath.Join(tmpDir, "BUILD"), []byte(""), 0600)
+
+	os.MkdirAll(filepath.Join(tmpDir, "tools"), os.ModeDir | 0700)
+	ioutil.WriteFile(filepath.Join(tmpDir, "tools", "bazel"), []byte(""), 0700)
+
+	entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, true)
+	expected := filepath.Join(tmpDir, "tools", "bazel")
+
+	if entrypoint != expected {
+		t.Fatalf("Expected to delegate bazel to %q, but got %q", expected, entrypoint)
+	}
+}
+
+func TestMaybeDelegateToPowershellWrapper(t *testing.T) {
+	tmpDir, err := ioutil.TempDir("", "TestMaybeDelegateToPowershellWrapper")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer os.RemoveAll(tmpDir)
+
+	os.MkdirAll(tmpDir, os.ModeDir | 0700)
+	ioutil.WriteFile(filepath.Join(tmpDir, "WORKSPACE"), []byte(""), 0600)
+	ioutil.WriteFile(filepath.Join(tmpDir, "BUILD"), []byte(""), 0600)
+
+	os.MkdirAll(filepath.Join(tmpDir, "tools"), os.ModeDir | 0700)
+	ioutil.WriteFile(filepath.Join(tmpDir, "tools", "bazel.ps1"), []byte(""), 0700)
+
+	entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, true)
+	expected := filepath.Join(tmpDir, "tools", "bazel.ps1")
+
+	// Only windows platforms use powershell wrappers
+	if runtime.GOOS != "windows" {
+		expected = "bazel_real"
+	}
+
+	if entrypoint != expected {
+		t.Fatalf("Expected to delegate bazel to %q, but got %q", expected, entrypoint)
+	}
+}
+
+func TestMaybeDelegateToBatchWrapper(t *testing.T) {
+	tmpDir, err := ioutil.TempDir("", "TestMaybeDelegateToBatchWrapper")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer os.RemoveAll(tmpDir)
+
+	os.MkdirAll(tmpDir, os.ModeDir | 0700)
+	ioutil.WriteFile(filepath.Join(tmpDir, "WORKSPACE"), []byte(""), 0600)
+	ioutil.WriteFile(filepath.Join(tmpDir, "BUILD"), []byte(""), 0600)
+
+	os.MkdirAll(filepath.Join(tmpDir, "tools"), os.ModeDir | 0700)
+	ioutil.WriteFile(filepath.Join(tmpDir, "tools", "bazel.bat"), []byte(""), 0700)
+
+	entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, true)
+	expected := filepath.Join(tmpDir, "tools", "bazel.bat")
+
+	// Only windows platforms use batch wrappers
+	if runtime.GOOS != "windows" {
+		expected = "bazel_real"
+	}
+
+	if entrypoint != expected {
+		t.Fatalf("Expected to delegate bazel to %q, but got %q", expected, entrypoint)
+	}
+}
+
+func TestMaybeDelegateToPowershellOverBatchWrapper(t *testing.T) {
+	tmpDir, err := ioutil.TempDir("", "TestMaybeDelegateToPowershellOverBatchWrapper")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer os.RemoveAll(tmpDir)
+
+	os.MkdirAll(tmpDir, os.ModeDir | 0700)
+	ioutil.WriteFile(filepath.Join(tmpDir, "WORKSPACE"), []byte(""), 0600)
+	ioutil.WriteFile(filepath.Join(tmpDir, "BUILD"), []byte(""), 0600)
+
+	os.MkdirAll(filepath.Join(tmpDir, "tools"), os.ModeDir | 0700)
+	ioutil.WriteFile(filepath.Join(tmpDir, "tools", "bazel.ps1"), []byte(""), 0700)
+	ioutil.WriteFile(filepath.Join(tmpDir, "tools", "bazel.bat"), []byte(""), 0700)
+
+	entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, true)
+	expected := filepath.Join(tmpDir, "tools", "bazel.ps1")
+
+	// Only windows platforms use powershell or batch wrappers
+	if runtime.GOOS != "windows" {
+		expected = "bazel_real"
+	}
+
+	if entrypoint != expected {
+		t.Fatalf("Expected to delegate bazel to %q, but got %q", expected, entrypoint)
+	}
+}