Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a full fs.FS implementation to runfiles #3969

Merged
merged 41 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
5b7c637
WIP: Add a full `fs.FS` implementation to `runfiles`
fmeum Jun 21, 2024
8b041e9
Extract out common implementation
fmeum Jun 21, 2024
febd9ea
Minimal feature parity for manifest implementation
fmeum Jun 21, 2024
b61cb31
Implement manifest except for dir statting plus test
fmeum Jun 21, 2024
41a2870
Test info
fmeum Jun 21, 2024
0cc3990
Add basic trie implementation
fmeum Jun 21, 2024
6d998b6
Fully fix test
fmeum Jun 21, 2024
af95615
Simplify
fmeum Jun 21, 2024
54c0991
Vendor testfs
fmeum Jun 22, 2024
6001d7a
FIx tests
fmeum Jun 22, 2024
3a038d5
Cleanup
fmeum Jun 23, 2024
0e1f8fd
Simplify
fmeum Jun 23, 2024
bf328b1
Root symlinks
fmeum Jun 24, 2024
179e51a
Simplify further
fmeum Jun 24, 2024
de2ca1a
Add comments and refactor
fmeum Jun 24, 2024
a00af32
Adopt test to Bzlmod
fmeum Jun 24, 2024
12b1d62
Also materialize canonical names
fmeum Jun 24, 2024
3479cc9
Adjust comment
fmeum Jun 24, 2024
7467cc4
Attempt to throw test failures
fmeum Jun 24, 2024
504fd0a
Attemp to fix WIndows #2
fmeum Jun 24, 2024
8eb58c0
Fix test #3
fmeum Jun 24, 2024
08f4ba8
Address review comments
fmeum Jun 24, 2024
36f434b
Fix test
fmeum Jun 24, 2024
a721602
Address review comments
fmeum Jun 25, 2024
f533967
Rename links for better test coverage
fmeum Jun 25, 2024
95dd35e
Rename manifest dir entries
fmeum Jun 25, 2024
6e37175
Add `String()` implementations and faithfully fake runfiles dir
fmeum Jun 25, 2024
9345a46
Resolve one layer of symlinks in directory impl
fmeum Jun 25, 2024
6d1fcd4
Resolve all symlinks
fmeum Jun 25, 2024
eec58b7
Update presubmit.yml
fmeum Jul 9, 2024
5b7cc84
Update presubmit.yml
fmeum Jul 9, 2024
05af0ca
Update BUILD.bazel
fmeum Jul 10, 2024
c7a1c7a
Update BUILD.bazel
fmeum Jul 10, 2024
17b5094
Update BUILD.bazel
fmeum Jul 10, 2024
07aadcd
Update BUILD.bazel
fmeum Jul 10, 2024
724e678
Update BUILD.bazel
fmeum Jul 10, 2024
43d6ff4
Update presubmit.yml
fmeum Jul 10, 2024
27ba95b
Update BUILD.bazel
fmeum Jul 10, 2024
a2c43a7
Update BUILD.bazel
fmeum Jul 10, 2024
cc1de78
Update BUILD.bazel
fmeum Jul 10, 2024
db45c96
Update BUILD.bazel
fmeum Jul 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .bazelci/presubmit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ tasks:
- "-//tests/core/nogo/bzlmod/..."
# Nogo includes/excludes doesn't work before bazel 6
- "-//tests/core/nogo/includes_excludes:includes_exclude_test"
# _repo_mapping is missing
- "-//tests/runfiles:runfiles_test"
ubuntu2004:
# enable some unflipped incompatible flags on this platform to ensure we don't regress.
shell_commands:
Expand Down Expand Up @@ -89,7 +91,7 @@ tasks:
- "@go_default_sdk//..."
test_targets:
- "//..."
macos:
macos_arm64:
shell_commands:
- tests/core/cgo/generate_imported_dylib.sh
build_flags:
Expand All @@ -106,6 +108,7 @@ tasks:
- "--apple_crosstool_top=@local_config_apple_cc//:toolchain"
- "--crosstool_top=@local_config_apple_cc//:toolchain"
- "--host_crosstool_top=@local_config_apple_cc//:toolchain"
- "--local_test_jobs=2"
fmeum marked this conversation as resolved.
Show resolved Hide resolved
test_targets:
- "//..."
rbe_ubuntu1604:
Expand All @@ -130,6 +133,8 @@ tasks:
- "--"
- "//..."
- "-//tests/core/stdlib:buildid_test"
# Source directories in runfiles are not supported with RBE.
- "-//tests/runfiles:runfiles_test"
windows:
build_flags:
- '--action_env=PATH=C:\tools\msys64\usr\bin;C:\tools\msys64\bin;C:\tools\msys64\mingw64\bin;C:\python3\Scripts\;C:\python3;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0;C:\Windows\System32\OpenSSH;C:\ProgramData\GooGet;C:\Program Files\Google\Compute Engine\metadata_scripts;C:\Program Files (x86)\Google\Cloud SDK\google-cloud-sdk\bin;C:\Program Files\Google\Compute Engine\sysprep;C:\ProgramData\chocolatey\bin;C:\Program Files\Git\cmd;C:\tools\msys64\usr\bin;c:\openjdk\bin;C:\Program Files (x86)\Windows Kits\8.1\Windows Performance Toolkit\;C:\Program Files\CMake\bin;c:\ninja;c:\bazel;c:\buildkite'
Expand Down
2 changes: 2 additions & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
startup --host_jvm_args=-DBAZEL_TRACK_SOURCE_DIRECTORIES=1

common --enable_platform_specific_config
# TODO: Temporarily disable while rules_go migrates to Bzlmod for its dev build.
# https://github.com/bazelbuild/bazel/issues/18958
Expand Down
44 changes: 43 additions & 1 deletion go/runfiles/directory.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@

package runfiles

import "path/filepath"
import (
"io/fs"
"os"
"path"
"path/filepath"
)

// Directory specifies the location of the runfiles directory. You can pass
// this as an option to New. If unset or empty, use the value of the
Expand All @@ -37,3 +42,40 @@ func (d Directory) new(sourceRepo SourceRepo) (*Runfiles, error) {
func (d Directory) path(s string) (string, error) {
return filepath.Join(string(d), filepath.FromSlash(s)), nil
}

func (d Directory) open(name string) (fs.File, error) {
dirFS := os.DirFS(string(d))
f, err := dirFS.Open(name)
if err != nil {
return nil, err
}
return &resolvedFile{f.(*os.File), func(child string) (fs.FileInfo, error) {
return fs.Stat(dirFS, path.Join(name, child))
}}, nil
}

type resolvedFile struct {
fs.ReadDirFile
lstatChildAfterReadlink func(string) (fs.FileInfo, error)
}

func (f *resolvedFile) ReadDir(n int) ([]fs.DirEntry, error) {
entries, err := f.ReadDirFile.ReadDir(n)
if err != nil {
return nil, err
}
for i, entry := range entries {
// Bazel runfiles directories consist of symlinks to the real files, which may themselves
// be directories. We want fs.WalkDir to descend into these directories as it does with the
// manifest implementation. We do this by replacing the information about an entry that is
// a symlink by the info of the resolved file.
if entry.Type()&fs.ModeSymlink != 0 {
info, err := f.lstatChildAfterReadlink(entry.Name())
if err != nil {
return nil, err
}
entries[i] = renamedDirEntry{fs.FileInfoToDirEntry(info), entry.Name()}
}
}
return entries, nil
}
170 changes: 127 additions & 43 deletions go/runfiles/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,63 +12,159 @@
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build go1.16
// +build go1.16

package runfiles

import (
"errors"
"io"
"io/fs"
"os"
"runtime"
"sort"
"strings"
"time"
)

// Open implements fs.FS.Open.
// Open implements fs.FS for a Runfiles instance.
//
// Rlocation-style paths are supported with both apparent and canonical repo
// names. The root directory of the filesystem (".") additionally lists the
// apparent repo names that are visible to the current source repo
// (with --enable_bzlmod).
func (r *Runfiles) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) {
// Required by testfs.TestFS.
if !fs.ValidPath(name) || (runtime.GOOS == "windows" && strings.ContainsRune(name, '\\')) {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
}
p, err := r.Rlocation(name)
if errors.Is(err, ErrEmpty) {
return emptyFile(name), nil
if name == "." {
return &rootDirFile{".", r, nil}, nil
}
repo, inRepoPath, hasInRepoPath := strings.Cut(name, "/")
key := repoMappingKey{r.sourceRepo, repo}
targetRepoDirectory, exists := r.repoMapping[key]
if !exists {
// Either name uses a canonical repo name or refers to a root symlink.
// In both cases, we can just open the file directly.
return r.impl.open(name)
}
// Construct the path with the target repo name replaced by the canonical
// name.
mappedPath := targetRepoDirectory
if hasInRepoPath {
mappedPath += "/" + inRepoPath
}
f, err := r.impl.open(mappedPath)
if err != nil {
return nil, pathError("open", name, err)
return nil, err
}
// The requested path is a child of a repo directory, return the unmodified
// file the implementation returned.
if hasInRepoPath {
return f, nil
}
return os.Open(p)
// Return a special file for a repo dir that knows its apparent name.
return &renamedFile{f, repo}, nil
}

// Stat implements fs.StatFS.Stat.
func (r *Runfiles) Stat(name string) (fs.FileInfo, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "stat", Path: name, Err: fs.ErrInvalid}
type rootDirFile struct {
dirFile
rf *Runfiles
entries []fs.DirEntry
}

func (r *rootDirFile) ReadDir(n int) ([]fs.DirEntry, error) {
if err := r.initEntries(); err != nil {
return nil, err
}
p, err := r.Rlocation(name)
if errors.Is(err, ErrEmpty) {
return emptyFileInfo(name), nil
if n > 0 && len(r.entries) == 0 {
return nil, io.EOF
}
if n <= 0 || n > len(r.entries) {
n = len(r.entries)
}
entries := r.entries[:n]
r.entries = r.entries[n:]
return entries, nil
}

func (r *rootDirFile) initEntries() error {
if r.entries != nil {
return nil
}
// The entries of the root dir should be the apparent names of the repos
// visible to the main repo (plus root symlinks). We thus need to read
// the real entries and then transform and filter them.
canonicalToApparentName := make(map[string]string)
for k, v := range r.rf.repoMapping {
if k.sourceRepo == r.rf.sourceRepo {
canonicalToApparentName[v] = k.targetRepoApparentName
}
}
rootFile, err := r.rf.impl.open(".")
if err != nil {
return nil, pathError("stat", name, err)
return err
}
return os.Stat(p)
realDirFile := rootFile.(fs.ReadDirFile)
fmeum marked this conversation as resolved.
Show resolved Hide resolved
realEntries, err := realDirFile.ReadDir(0)
if err != nil {
return err
}
for _, e := range realEntries {
r.entries = append(r.entries, e)
if apparent, ok := canonicalToApparentName[e.Name()]; ok && e.IsDir() && apparent != e.Name() {
// A repo directory that is visible to the current source repo is additionally
// materialized under its apparent name. We do not use a symlink as
// fs.WalkDir doesn't descend into symlinks.
r.entries = append(r.entries, renamedDirEntry{e, apparent})
fmeum marked this conversation as resolved.
Show resolved Hide resolved
}
}
sort.Slice(r.entries, func(i, j int) bool {
return r.entries[i].Name() < r.entries[j].Name()
})
return nil
}

// ReadFile implements fs.ReadFileFS.ReadFile.
func (r *Runfiles) ReadFile(name string) ([]byte, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
type renamedFile struct {
fs.File
name string
}

func (r renamedFile) Stat() (fs.FileInfo, error) {
info, err := r.File.Stat()
if err != nil {
return nil, err
}
p, err := r.Rlocation(name)
if errors.Is(err, ErrEmpty) {
return nil, nil
return renamedFileInfo{info, r.name}, nil
}

func (r renamedFile) ReadDir(n int) ([]fs.DirEntry, error) {
readDirFile, ok := r.File.(fs.ReadDirFile)
if !ok {
return nil, &fs.PathError{Op: "readdir", Path: r.name, Err: fs.ErrInvalid}
}
return readDirFile.ReadDir(n)
}

type renamedDirEntry struct {
fs.DirEntry
name string
}

func (r renamedDirEntry) Name() string { return r.name }
func (r renamedDirEntry) Info() (fs.FileInfo, error) {
info, err := r.DirEntry.Info()
if err != nil {
return nil, pathError("open", name, err)
return nil, err
}
return os.ReadFile(p)
return renamedFileInfo{info, r.name}, nil
}
func (r renamedDirEntry) String() string { return fs.FormatDirEntry(r) }

type renamedFileInfo struct {
fs.FileInfo
name string
}

func (r renamedFileInfo) Name() string { return r.name }
func (r renamedFileInfo) String() string { return fs.FormatFileInfo(r) }

type emptyFile string

Expand All @@ -84,16 +180,4 @@ func (emptyFileInfo) Mode() fs.FileMode { return 0444 }
func (emptyFileInfo) ModTime() time.Time { return time.Time{} }
func (emptyFileInfo) IsDir() bool { return false }
func (emptyFileInfo) Sys() interface{} { return nil }

func pathError(op, name string, err error) error {
if err == nil {
return nil
}
var rerr Error
if errors.As(err, &rerr) {
// Unwrap the error because we don’t need the failing name
// twice.
return &fs.PathError{Op: op, Path: rerr.Name, Err: rerr.Err}
}
return &fs.PathError{Op: op, Path: name, Err: err}
}
func (i emptyFileInfo) String() string { return fs.FormatFileInfo(i) }
Loading