| // datafile provides an abstraction for accessing files passed through the data |
| // attribute in a Bazel build rule. |
| // |
| // It thinly wraps around the Bazel/Go runfile library (to allow running from |
| // outside `bazel run`). |
| package datafile |
| |
| import ( |
| "bufio" |
| "fmt" |
| "log" |
| "os" |
| "path/filepath" |
| "strings" |
| |
| "github.com/bazelbuild/rules_go/go/tools/bazel" |
| ) |
| |
| // parseManifest takes a bazel runfile MANIFEST and parses it into a map from |
| // workspace-relative path to absolute path, flattening all workspaces into a |
| // single tree. |
| func parseManifest(path string) (map[string]string, error) { |
| f, err := os.Open(path) |
| if err != nil { |
| return nil, fmt.Errorf("could not open MANIFEST: %v", err) |
| } |
| defer f.Close() |
| |
| manifest := make(map[string]string) |
| scanner := bufio.NewScanner(f) |
| for scanner.Scan() { |
| parts := strings.Split(scanner.Text(), " ") |
| if len(parts) != 2 { |
| continue |
| } |
| fpathParts := strings.Split(parts[0], string(os.PathSeparator)) |
| fpath := strings.Join(fpathParts[1:], string(os.PathSeparator)) |
| manifest[fpath] = parts[1] |
| } |
| return manifest, nil |
| } |
| |
| // resolveRunfile tries to resolve a workspace-relative file path into an |
| // absolute path with the use of bazel runfiles, through either the original |
| // Bazel/Go runfile integration or a wrapper that also supports running from |
| // outside `bazel run`. |
| func resolveRunfile(path string) (string, error) { |
| var errEx error |
| ep, err := os.Executable() |
| if err == nil { |
| rfdir := ep + ".runfiles" |
| mfpath := filepath.Join(rfdir, "MANIFEST") |
| if stat, err := os.Stat(rfdir); err == nil && stat.IsDir() { |
| // We have a runfiles directory, parse MANIFEST and resolve files this way. |
| manifest, err := parseManifest(mfpath) |
| if err == nil { |
| tpath := manifest[path] |
| if tpath == "" { |
| errEx = fmt.Errorf("not in MANIFEST") |
| } else { |
| return tpath, err |
| } |
| } else { |
| errEx = err |
| } |
| } else { |
| errEx = err |
| } |
| } |
| |
| // Try runfiles just in case. |
| rf, errRF := bazel.Runfile(path) |
| if errRF == nil { |
| return rf, nil |
| } |
| return "", fmt.Errorf("could not resolve via executable location (%v) and runfile resolution failed: %v", errEx, errRF) |
| } |
| |
| // Get tries to read a workspace-relative file path through the use of Bazel |
| // runfiles, including for cases when executables are running outside `bazel |
| // run`. |
| func Get(path string) ([]byte, error) { |
| rfpath, err := resolveRunfile(path) |
| if err != nil { |
| return nil, err |
| } |
| return os.ReadFile(rfpath) |
| } |
| |
| // MustGet either successfully resolves a file through Get() or logs an error |
| // (through the stdlib log library) and stops execution. This should thus only |
| // be used in binaries which use the log library. |
| func MustGet(path string) []byte { |
| res, err := Get(path) |
| if err != nil { |
| log.Fatalf("Could not get datafile %s: %v", path, err) |
| } |
| return res |
| } |