blob: f8f2ef9fb86868390bc1cec1b7b48081a09a768a [file] [log] [blame]
Serge Bazanski0f8e5a22021-10-16 20:53:51 +00001package workspace
2
3import (
4 "fmt"
5 "os"
6 "path"
7 "sync"
8 "syscall"
9)
10
11// isWorkspace returns whether a given string is a valid path pointing to a
12// Bazel workspace directory.
13func isWorkspace(dir string) bool {
14 w := path.Join(dir, "WORKSPACE")
15 if _, err := os.Stat(w); err == nil {
16 return true
17 }
18 return false
19}
20
21// getPathFSID returns an opaque filesystem identifier for a given path.
22func getPathFSID(dir string) (uint64, error) {
23 st, err := os.Stat(dir)
24 if err != nil {
25 // No need to wrap err, as stat errors are already quite explicit
26 // (they also include the stat'd path).
27 return 0, err
28 }
29 switch x := st.Sys().(type) {
30 case *syscall.Stat_t:
31 return x.Dev, nil
32 default:
33 return 0, fmt.Errorf("unsupported operating system (got stat type %+v)", st.Sys())
34 }
35}
36
37// lookForWorkspace recurses up a directory until it finds a WORKSPACE, the
38// root, or crosses a filesystem boundary.
39func lookForWorkspace(dir string) (string, error) {
40 fsid, err := getPathFSID(dir)
41 if err != nil {
42 return "", fmt.Errorf("could not get initial FSID: %w", err)
43 }
44 for {
45 if dir == "." || dir == "/" || dir == "" {
46 return "", fmt.Errorf("got up to root before finding workspace")
47 }
48
49 fsid2, err := getPathFSID(dir)
50 if err != nil {
51 return "", fmt.Errorf("could not get parent FWID: %w", err)
52 }
53 if fsid2 != fsid {
54 return "", fmt.Errorf("crossed filesystem boundaries before finding workspace")
55 }
56
57 if isWorkspace(dir) {
58 return dir, nil
59 }
60 dir = path.Dir(dir)
61 }
62}
63
64// Get returns the workspace directory from which a given
65// command line tool is running. This handles the following
66// cases:
67//
68// 1. The command line tool was invoked via `bazel run`.
69// 2. The command line tool was started in the workspace directory or a
70// subdirectory.
71//
72// If the workspace directory path cannot be inferred based on the above
73// assumptions, an error is returned.
74func Get() (string, error) {
75 workspaceOnce.Do(func() {
76 workspace, workspaceErr = get()
77 })
78 return workspace, workspaceErr
79}
80
81var (
82 workspace string
83 workspaceErr error
84 workspaceOnce sync.Once
85)
86
87func get() (string, error) {
88 if p := os.Getenv("BUILD_WORKSPACE_DIRECTORY"); p != "" && isWorkspace(p) {
89 return p, nil
90 }
91
92 wd, err := os.Getwd()
93 if err != nil {
94 return "", err
95 }
96 p, err := lookForWorkspace(wd)
97 if err != nil {
98 return "", fmt.Errorf("not invoked from `bazel run` and could not find workspace root by traversing upwards: %w", err)
99 }
100 return p, nil
101}