1// Copyright 2018 The Bazel Authors. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package bazel 16 17import ( 18 "bytes" 19 "errors" 20 "fmt" 21 "io/ioutil" 22 "os" 23 "path" 24 "path/filepath" 25 "runtime" 26 "sort" 27 "strings" 28 "sync" 29) 30 31const ( 32 RUNFILES_MANIFEST_FILE = "RUNFILES_MANIFEST_FILE" 33 RUNFILES_DIR = "RUNFILES_DIR" 34) 35 36// Runfile returns an absolute path to the file named by "path", which 37// should be a relative path from the workspace root to the file within 38// the bazel workspace. 39// 40// Runfile may be called from tests invoked with 'bazel test' and 41// binaries invoked with 'bazel run'. On Windows, 42// only tests invoked with 'bazel test' are supported. 43// 44// Deprecated: Use github.com/bazelbuild/rules_go/go/runfiles instead for 45// cross-platform support matching the behavior of the Bazel-provided runfiles 46// libraries. 47func Runfile(path string) (string, error) { 48 // Search in working directory 49 if _, err := os.Stat(path); err == nil { 50 return filepath.Abs(path) 51 } 52 53 if err := ensureRunfiles(); err != nil { 54 return "", err 55 } 56 57 // Search manifest if we have one. 58 if entry, ok := runfiles.index.GetIgnoringWorkspace(path); ok { 59 return entry.Path, nil 60 } 61 62 if strings.HasPrefix(path, "../") || strings.HasPrefix(path, "external/") { 63 pathParts := strings.Split(path, "/") 64 if len(pathParts) >= 3 { 65 workspace := pathParts[1] 66 pathInsideWorkspace := strings.Join(pathParts[2:], "/") 67 if path := runfiles.index.Get(workspace, pathInsideWorkspace); path != "" { 68 return path, nil 69 } 70 } 71 } 72 73 // Search the main workspace. 74 if runfiles.workspace != "" { 75 mainPath := filepath.Join(runfiles.dir, runfiles.workspace, path) 76 if _, err := os.Stat(mainPath); err == nil { 77 return mainPath, nil 78 } 79 } 80 81 // Search other workspaces. 82 for _, w := range runfiles.workspaces { 83 workPath := filepath.Join(runfiles.dir, w, path) 84 if _, err := os.Stat(workPath); err == nil { 85 return workPath, nil 86 } 87 } 88 89 return "", fmt.Errorf("Runfile %s: could not locate file", path) 90} 91 92// FindBinary returns an absolute path to the binary built from a go_binary 93// rule in the given package with the given name. FindBinary is similar to 94// Runfile, but it accounts for varying configurations and file extensions, 95// which may cause the binary to have different paths on different platforms. 96// 97// FindBinary may be called from tests invoked with 'bazel test' and 98// binaries invoked with 'bazel run'. On Windows, 99// only tests invoked with 'bazel test' are supported. 100func FindBinary(pkg, name string) (string, bool) { 101 if err := ensureRunfiles(); err != nil { 102 return "", false 103 } 104 105 // If we've gathered a list of runfiles, either by calling ListRunfiles or 106 // parsing the manifest on Windows, just use that instead of searching 107 // directories. Return the first match. The manifest on Windows may contain 108 // multiple entries for the same file. 109 if runfiles.list != nil { 110 if runtime.GOOS == "windows" { 111 name += ".exe" 112 } 113 for _, entry := range runfiles.list { 114 if path.Base(entry.ShortPath) != name { 115 continue 116 } 117 pkgDir := path.Dir(path.Dir(entry.ShortPath)) 118 if pkgDir == "." { 119 pkgDir = "" 120 } 121 if pkgDir != pkg { 122 continue 123 } 124 return entry.Path, true 125 } 126 return "", false 127 } 128 129 dir, err := Runfile(pkg) 130 if err != nil { 131 return "", false 132 } 133 var found string 134 stopErr := errors.New("stop") 135 err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 136 if err != nil { 137 return err 138 } 139 if info.IsDir() { 140 return nil 141 } 142 base := filepath.Base(path) 143 stem := strings.TrimSuffix(base, ".exe") 144 if stem != name { 145 return nil 146 } 147 if runtime.GOOS != "windows" { 148 if st, err := os.Stat(path); err != nil { 149 return err 150 } else if st.Mode()&0111 == 0 { 151 return nil 152 } 153 } 154 if stem == name { 155 found = path 156 return stopErr 157 } 158 return nil 159 }) 160 if err == stopErr { 161 return found, true 162 } else { 163 return "", false 164 } 165} 166 167// A RunfileEntry describes a runfile. 168type RunfileEntry struct { 169 // Workspace is the bazel workspace the file came from. For example, 170 // this would be "io_bazel_rules_go" for a file in rules_go. 171 Workspace string 172 173 // ShortPath is a relative, slash-separated path from the workspace root 174 // to the file. For non-binary files, this may be passed to Runfile 175 // to locate a file. 176 ShortPath string 177 178 // Path is an absolute path to the file. 179 Path string 180} 181 182// ListRunfiles returns a list of available runfiles. 183func ListRunfiles() ([]RunfileEntry, error) { 184 if err := ensureRunfiles(); err != nil { 185 return nil, err 186 } 187 188 if runfiles.list == nil && runfiles.dir != "" { 189 runfiles.listOnce.Do(func() { 190 var list []RunfileEntry 191 haveWorkspaces := strings.HasSuffix(runfiles.dir, ".runfiles") && runfiles.workspace != "" 192 193 err := filepath.Walk(runfiles.dir, func(path string, info os.FileInfo, err error) error { 194 if err != nil { 195 return err 196 } 197 rel, _ := filepath.Rel(runfiles.dir, path) 198 rel = filepath.ToSlash(rel) 199 if rel == "." { 200 return nil 201 } 202 203 var workspace, shortPath string 204 if haveWorkspaces { 205 if i := strings.IndexByte(rel, '/'); i < 0 { 206 return nil 207 } else { 208 workspace, shortPath = rel[:i], rel[i+1:] 209 } 210 } else { 211 workspace, shortPath = "", rel 212 } 213 214 list = append(list, RunfileEntry{Workspace: workspace, ShortPath: shortPath, Path: path}) 215 return nil 216 }) 217 if err != nil { 218 runfiles.err = err 219 return 220 } 221 runfiles.list = list 222 }) 223 } 224 return runfiles.list, runfiles.err 225} 226 227// TestWorkspace returns the name of the Bazel workspace for this test. 228// TestWorkspace returns an error if the TEST_WORKSPACE environment variable 229// was not set or SetDefaultTestWorkspace was not called. 230func TestWorkspace() (string, error) { 231 if err := ensureRunfiles(); err != nil { 232 return "", err 233 } 234 if runfiles.workspace != "" { 235 return runfiles.workspace, nil 236 } 237 return "", errors.New("TEST_WORKSPACE not set and SetDefaultTestWorkspace not called") 238} 239 240// SetDefaultTestWorkspace allows you to set a fake value for the 241// environment variable TEST_WORKSPACE if it is not defined. This is useful 242// when running tests on the command line and not through Bazel. 243func SetDefaultTestWorkspace(w string) { 244 ensureRunfiles() 245 runfiles.workspace = w 246} 247 248// RunfilesPath return the path to the runfiles tree. 249// It will return an error if there is no runfiles tree, for example because 250// the executable is run on Windows or was not invoked with 'bazel test' 251// or 'bazel run'. 252func RunfilesPath() (string, error) { 253 if err := ensureRunfiles(); err != nil { 254 return "", err 255 } 256 if runfiles.dir == "" { 257 if runtime.GOOS == "windows" { 258 return "", errors.New("RunfilesPath: no runfiles directory on windows") 259 } else { 260 return "", errors.New("could not locate runfiles directory") 261 } 262 } 263 if runfiles.workspace == "" { 264 return "", errors.New("could not locate runfiles workspace") 265 } 266 return filepath.Join(runfiles.dir, runfiles.workspace), nil 267} 268 269var runfiles = struct { 270 once, listOnce sync.Once 271 272 // list is a list of known runfiles, either loaded from the manifest 273 // or discovered by walking the runfile directory. 274 list []RunfileEntry 275 276 // index maps runfile short paths to absolute paths. 277 index index 278 279 // dir is a path to the runfile directory. Typically this is a directory 280 // named <target>.runfiles, with a subdirectory for each workspace. 281 dir string 282 283 // workspace is workspace where the binary or test was built. 284 workspace string 285 286 // workspaces is a list of other workspace names. 287 workspaces []string 288 289 // err is set when there is an error loading runfiles, for example, 290 // parsing the manifest. 291 err error 292}{} 293 294type index struct { 295 indexWithWorkspace map[indexKey]*RunfileEntry 296 indexIgnoringWorksapce map[string]*RunfileEntry 297} 298 299func newIndex() index { 300 return index{ 301 indexWithWorkspace: make(map[indexKey]*RunfileEntry), 302 indexIgnoringWorksapce: make(map[string]*RunfileEntry), 303 } 304} 305 306func (i *index) Put(entry *RunfileEntry) { 307 i.indexWithWorkspace[indexKey{ 308 workspace: entry.Workspace, 309 shortPath: entry.ShortPath, 310 }] = entry 311 i.indexIgnoringWorksapce[entry.ShortPath] = entry 312} 313 314func (i *index) Get(workspace string, shortPath string) string { 315 entry := i.indexWithWorkspace[indexKey{ 316 workspace: workspace, 317 shortPath: shortPath, 318 }] 319 if entry == nil { 320 return "" 321 } 322 return entry.Path 323} 324 325func (i *index) GetIgnoringWorkspace(shortPath string) (*RunfileEntry, bool) { 326 entry, ok := i.indexIgnoringWorksapce[shortPath] 327 return entry, ok 328} 329 330type indexKey struct { 331 workspace string 332 shortPath string 333} 334 335func ensureRunfiles() error { 336 runfiles.once.Do(initRunfiles) 337 return runfiles.err 338} 339 340func initRunfiles() { 341 manifest := os.Getenv("RUNFILES_MANIFEST_FILE") 342 if manifest != "" { 343 // On Windows, Bazel doesn't create a symlink tree of runfiles because 344 // Windows doesn't support symbolic links by default. Instead, runfile 345 // locations are written to a manifest file. 346 runfiles.index = newIndex() 347 data, err := ioutil.ReadFile(manifest) 348 if err != nil { 349 runfiles.err = err 350 return 351 } 352 lineno := 0 353 for len(data) > 0 { 354 i := bytes.IndexByte(data, '\n') 355 var line []byte 356 if i < 0 { 357 line = data 358 data = nil 359 } else { 360 line = data[:i] 361 data = data[i+1:] 362 } 363 lineno++ 364 365 // Only TrimRight newlines. Do not TrimRight() completely, because that would remove spaces too. 366 // This is necessary in order to have at least one space in every manifest line. 367 // Some manifest entries don't have any path after this space, namely the "__init__.py" entries. 368 // original comment sourced from: https://github.com/bazelbuild/bazel/blob/09c621e4cf5b968f4c6cdf905ab142d5961f9ddc/src/test/py/bazel/runfiles_test.py#L225 369 line = bytes.TrimRight(line, "\r\n") 370 if len(line) == 0 { 371 continue 372 } 373 374 spaceIndex := bytes.IndexByte(line, ' ') 375 if spaceIndex < 0 { 376 runfiles.err = fmt.Errorf( 377 "error parsing runfiles manifest: %s:%d: no space: '%s'", manifest, lineno, line) 378 return 379 } 380 shortPath := string(line[0:spaceIndex]) 381 abspath := "" 382 if len(line) > spaceIndex+1 { 383 abspath = string(line[spaceIndex+1:]) 384 } 385 386 entry := RunfileEntry{ShortPath: shortPath, Path: abspath} 387 if i := strings.IndexByte(entry.ShortPath, '/'); i >= 0 { 388 entry.Workspace = entry.ShortPath[:i] 389 entry.ShortPath = entry.ShortPath[i+1:] 390 } 391 if strings.HasPrefix(entry.ShortPath, "external/") { 392 entry.ShortPath = entry.ShortPath[len("external/"):] 393 if i := strings.IndexByte(entry.ShortPath, '/'); i >= 0 { 394 entry.Workspace = entry.ShortPath[:i] 395 entry.ShortPath = entry.ShortPath[i+1:] 396 } 397 } 398 399 runfiles.list = append(runfiles.list, entry) 400 runfiles.index.Put(&entry) 401 } 402 } 403 404 runfiles.workspace = os.Getenv("TEST_WORKSPACE") 405 406 if dir := os.Getenv("RUNFILES_DIR"); dir != "" { 407 runfiles.dir = dir 408 } else if dir = os.Getenv("TEST_SRCDIR"); dir != "" { 409 runfiles.dir = dir 410 } else if runtime.GOOS != "windows" { 411 dir, err := os.Getwd() 412 if err != nil { 413 runfiles.err = fmt.Errorf("error locating runfiles dir: %v", err) 414 return 415 } 416 417 parent := filepath.Dir(dir) 418 if strings.HasSuffix(parent, ".runfiles") { 419 runfiles.dir = parent 420 if runfiles.workspace == "" { 421 runfiles.workspace = filepath.Base(dir) 422 } 423 } else { 424 runfiles.err = errors.New("could not locate runfiles directory") 425 return 426 } 427 } 428 429 if runfiles.dir != "" { 430 fis, err := ioutil.ReadDir(runfiles.dir) 431 if err != nil { 432 runfiles.err = fmt.Errorf("could not open runfiles directory: %v", err) 433 return 434 } 435 for _, fi := range fis { 436 if fi.IsDir() { 437 runfiles.workspaces = append(runfiles.workspaces, fi.Name()) 438 } 439 } 440 sort.Strings(runfiles.workspaces) 441 } 442} 443