diff --git a/MODULE.bazel b/MODULE.bazel index ae13e21c..574eb9d4 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -23,7 +23,7 @@ git_override( git_override( module_name = "com_github_buildbarn_bb_storage", - commit = "6002cad335378bbdcdda4225d47c7dc2c08a6d94", + commit = "6d689475d039c4ac5984f977066cb5249f6a9c6f", remote = "https://github.com/buildbarn/bb-storage.git", ) diff --git a/cmd/bb_noop_worker/BUILD.bazel b/cmd/bb_noop_worker/BUILD.bazel index 966f7def..e672978b 100644 --- a/cmd/bb_noop_worker/BUILD.bazel +++ b/cmd/bb_noop_worker/BUILD.bazel @@ -18,6 +18,7 @@ go_library( "@com_github_buildbarn_bb_storage//pkg/global", "@com_github_buildbarn_bb_storage//pkg/program", "@com_github_buildbarn_bb_storage//pkg/util", + "@com_github_buildbarn_bb_storage//pkg/zstd", "@org_golang_google_grpc//codes", "@org_golang_google_grpc//status", ], diff --git a/cmd/bb_noop_worker/main.go b/cmd/bb_noop_worker/main.go index fb94ce4d..e2f690c8 100644 --- a/cmd/bb_noop_worker/main.go +++ b/cmd/bb_noop_worker/main.go @@ -16,6 +16,7 @@ import ( "github.com/buildbarn/bb-storage/pkg/global" "github.com/buildbarn/bb-storage/pkg/program" "github.com/buildbarn/bb-storage/pkg/util" + bb_zstd "github.com/buildbarn/bb-storage/pkg/zstd" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -48,7 +49,8 @@ func main() { configuration.ContentAddressableStorage, blobstore_configuration.NewCASBlobAccessCreator( grpcClientFactory, - int(configuration.MaximumMessageSizeBytes))) + int(configuration.MaximumMessageSizeBytes), + bb_zstd.NewPoolFromConfiguration(nil))) if err != nil { return util.StatusWrap(err, "Failed to create Content Adddressable Storage") } diff --git a/cmd/bb_scheduler/BUILD.bazel b/cmd/bb_scheduler/BUILD.bazel index e33a3f83..c312f827 100644 --- a/cmd/bb_scheduler/BUILD.bazel +++ b/cmd/bb_scheduler/BUILD.bazel @@ -50,6 +50,7 @@ go_library( "@com_github_buildbarn_bb_storage//pkg/proto/iscc", "@com_github_buildbarn_bb_storage//pkg/random", "@com_github_buildbarn_bb_storage//pkg/util", + "@com_github_buildbarn_bb_storage//pkg/zstd", "@com_github_google_uuid//:uuid", "@com_github_gorilla_mux//:mux", "@org_golang_google_grpc//:grpc", diff --git a/cmd/bb_scheduler/main.go b/cmd/bb_scheduler/main.go index b586fc5c..3aa4d60d 100644 --- a/cmd/bb_scheduler/main.go +++ b/cmd/bb_scheduler/main.go @@ -28,6 +28,7 @@ import ( "github.com/buildbarn/bb-storage/pkg/proto/iscc" "github.com/buildbarn/bb-storage/pkg/random" "github.com/buildbarn/bb-storage/pkg/util" + bb_zstd "github.com/buildbarn/bb-storage/pkg/zstd" "github.com/google/uuid" "github.com/gorilla/mux" @@ -63,7 +64,8 @@ func main() { configuration.ContentAddressableStorage, blobstore_configuration.NewCASBlobAccessCreator( grpcClientFactory, - int(configuration.MaximumMessageSizeBytes))) + int(configuration.MaximumMessageSizeBytes), + bb_zstd.NewPoolFromConfiguration(nil))) if err != nil { return util.StatusWrap(err, "Failed to create Content Adddressable Storage") } diff --git a/cmd/bb_worker/BUILD.bazel b/cmd/bb_worker/BUILD.bazel index 095b60b9..c243ead6 100644 --- a/cmd/bb_worker/BUILD.bazel +++ b/cmd/bb_worker/BUILD.bazel @@ -35,6 +35,7 @@ go_library( "@com_github_buildbarn_bb_storage//pkg/program", "@com_github_buildbarn_bb_storage//pkg/random", "@com_github_buildbarn_bb_storage//pkg/util", + "@com_github_buildbarn_bb_storage//pkg/zstd", "@com_github_google_uuid//:uuid", "@io_opentelemetry_go_otel//:otel", "@org_golang_google_grpc//codes", diff --git a/cmd/bb_worker/main.go b/cmd/bb_worker/main.go index 0dd047e9..25214aba 100644 --- a/cmd/bb_worker/main.go +++ b/cmd/bb_worker/main.go @@ -38,6 +38,7 @@ import ( "github.com/buildbarn/bb-storage/pkg/program" "github.com/buildbarn/bb-storage/pkg/random" "github.com/buildbarn/bb-storage/pkg/util" + bb_zstd "github.com/buildbarn/bb-storage/pkg/zstd" "github.com/google/uuid" "golang.org/x/sync/semaphore" @@ -88,7 +89,8 @@ func main() { dependenciesGroup, configuration.Blobstore, grpcClientFactory, - int(configuration.MaximumMessageSizeBytes)) + int(configuration.MaximumMessageSizeBytes), + bb_zstd.NewPoolFromConfiguration(nil)) if err != nil { return err } diff --git a/go.mod b/go.mod index 7a7f1591..4b43f432 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/bazelbuild/buildtools v0.0.0-20260211083412-859bfffeef82 github.com/bazelbuild/remote-apis v0.0.0-20260216160025-715b73f3f9e4 github.com/bazelbuild/rules_go v0.59.0 - github.com/buildbarn/bb-storage v0.0.0-20260303071613-6002cad33537 + github.com/buildbarn/bb-storage v0.0.0-20260316192935-6d689475d039 github.com/buildbarn/go-xdr v0.0.0-20240702182809-236788cf9e89 github.com/golang/protobuf v1.5.4 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index 32bb7ab5..6b80263a 100644 --- a/go.sum +++ b/go.sum @@ -79,8 +79,8 @@ github.com/bazelbuild/rules_go v0.59.0/go.mod h1:Pn30cb4M513fe2rQ6GiJ3q8QyrRsgC7 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/buildbarn/bb-storage v0.0.0-20260303071613-6002cad33537 h1:81k5KiFlYyaQaheb5pe9p/nz4Ao8jGWcNK5LC9z+8vE= -github.com/buildbarn/bb-storage v0.0.0-20260303071613-6002cad33537/go.mod h1:96kqnkrdkHHi94Agje3NM8qwrYMxJRSkAqsb7oXRhNI= +github.com/buildbarn/bb-storage v0.0.0-20260316192935-6d689475d039 h1:vqWwW9H2R6ACSWkJisorYsSn9qsAbv9nbjqMM0BJhyI= +github.com/buildbarn/bb-storage v0.0.0-20260316192935-6d689475d039/go.mod h1:96kqnkrdkHHi94Agje3NM8qwrYMxJRSkAqsb7oXRhNI= github.com/buildbarn/go-sha256tree v0.0.0-20250310211320-0f70f20e855b h1:IKUxixGBm9UxobU7c248z0BF0ojG19uoSLz8MFZM/KA= github.com/buildbarn/go-sha256tree v0.0.0-20250310211320-0f70f20e855b/go.mod h1:e7g3/yWApcg+PpDqd4eQEEV8pexQmfCgK3frP+1Wuvk= github.com/buildbarn/go-xdr v0.0.0-20240702182809-236788cf9e89 h1:Wtpgk4CIkoEJ7Qx3BwjaMp3TOVv834heqyCC9jMKStM= diff --git a/pkg/filesystem/virtual/BUILD.bazel b/pkg/filesystem/virtual/BUILD.bazel index f03a16ae..6b262e7c 100644 --- a/pkg/filesystem/virtual/BUILD.bazel +++ b/pkg/filesystem/virtual/BUILD.bazel @@ -26,6 +26,8 @@ go_library( "leaf.go", "linkable_leaf.go", "named_attributes_factory.go", + "native_symlink_target_unix.go", + "native_symlink_target_windows.go", "nfs_handle_allocator.go", "node.go", "permissions.go", diff --git a/pkg/filesystem/virtual/base_symlink_factory.go b/pkg/filesystem/virtual/base_symlink_factory.go index fd030110..1d4c8c10 100644 --- a/pkg/filesystem/virtual/base_symlink_factory.go +++ b/pkg/filesystem/virtual/base_symlink_factory.go @@ -32,7 +32,7 @@ func (f symlink) readlinkParser() (path.Parser, error) { if !utf8.Valid(f.target) { return nil, status.Error(codes.InvalidArgument, "Symbolic link contents are not valid UTF-8") } - return path.UNIXFormat.NewParser(string(f.target)), nil + return path.LocalFormat.NewParser(string(f.target)), nil } func (f symlink) readlinkString() (string, error) { diff --git a/pkg/filesystem/virtual/cas_initial_contents_fetcher.go b/pkg/filesystem/virtual/cas_initial_contents_fetcher.go index 1767722e..84f02e97 100644 --- a/pkg/filesystem/virtual/cas_initial_contents_fetcher.go +++ b/pkg/filesystem/virtual/cas_initial_contents_fetcher.go @@ -110,7 +110,14 @@ func (icf *casInitialContentsFetcher) fetchContentsUnwrapped(fileReadMonitorFact return nil, status.Errorf(codes.InvalidArgument, "Directory contains multiple children named %#v", entry.Name) } - leaf := icf.options.symlinkFactory.LookupSymlink([]byte(entry.Target)) + // REv2 symlink targets use UNIX path separators. Convert + // to the native platform format so that the stored bytes + // can be returned verbatim by VirtualReadlink. + target, err := nativeSymlinkTarget(entry.Target) + if err != nil { + return nil, util.StatusWrapf(err, "Failed to normalize target of symlink %#v", entry.Name) + } + leaf := icf.options.symlinkFactory.LookupSymlink(target) children[component] = InitialChild{}.FromLeaf(leaf) leavesToUnlink = append(leavesToUnlink, leaf) } diff --git a/pkg/filesystem/virtual/native_symlink_target_unix.go b/pkg/filesystem/virtual/native_symlink_target_unix.go new file mode 100644 index 00000000..d2e63b27 --- /dev/null +++ b/pkg/filesystem/virtual/native_symlink_target_unix.go @@ -0,0 +1,10 @@ +//go:build !windows + +package virtual + +// nativeSymlinkTarget converts a REv2 symlink target (UNIX format) +// to the native platform format. On UNIX this is a no-op, as REv2 +// already uses UNIX path separators. +func nativeSymlinkTarget(target string) ([]byte, error) { + return []byte(target), nil +} diff --git a/pkg/filesystem/virtual/native_symlink_target_windows.go b/pkg/filesystem/virtual/native_symlink_target_windows.go new file mode 100644 index 00000000..21f8a121 --- /dev/null +++ b/pkg/filesystem/virtual/native_symlink_target_windows.go @@ -0,0 +1,22 @@ +//go:build windows + +package virtual + +import "github.com/buildbarn/bb-storage/pkg/filesystem/path" + +// nativeSymlinkTarget converts a REv2 symlink target (UNIX format) +// to the native platform format. On Windows this converts forward +// slashes to backslashes and strips trailing directory separators +// that would cause issues in reparse point substitute names. +func nativeSymlinkTarget(target string) ([]byte, error) { + targetParser := path.UNIXFormat.NewParser(target) + targetPath, scopeWalker := path.EmptyBuilder.Join(path.VoidScopeWalker) + if err := path.Resolve(targetParser, scopeWalker); err != nil { + return nil, err + } + s, err := targetPath.GetWindowsString(path.WindowsPathFormatStandard) + if err != nil { + return nil, err + } + return []byte(s), nil +} diff --git a/pkg/filesystem/virtual/winfsp/BUILD.bazel b/pkg/filesystem/virtual/winfsp/BUILD.bazel index 59d64340..969e8dea 100644 --- a/pkg/filesystem/virtual/winfsp/BUILD.bazel +++ b/pkg/filesystem/virtual/winfsp/BUILD.bazel @@ -68,12 +68,17 @@ go_test( tags = ["manual"], deps = select({ "@rules_go//go/platform:windows": [ + "//pkg/cas", "//pkg/filesystem/pool", "//pkg/filesystem/virtual", "//pkg/filesystem/virtual/configuration", "//pkg/proto/configuration/filesystem/virtual", + "@bazel_remote_apis//build/bazel/remote/execution/v2:remote_execution_go_proto", "@com_github_buildbarn_bb_storage//pkg/blockdevice", "@com_github_buildbarn_bb_storage//pkg/clock", + "@com_github_buildbarn_bb_storage//pkg/digest", + "@com_github_buildbarn_bb_storage//pkg/filesystem", + "@com_github_buildbarn_bb_storage//pkg/filesystem/path", "@com_github_buildbarn_bb_storage//pkg/program", "@com_github_buildbarn_bb_storage//pkg/util", "@com_github_stretchr_testify//require", diff --git a/pkg/filesystem/virtual/winfsp/file_system.go b/pkg/filesystem/virtual/winfsp/file_system.go index 33ff5084..d9302a52 100644 --- a/pkg/filesystem/virtual/winfsp/file_system.go +++ b/pkg/filesystem/virtual/winfsp/file_system.go @@ -1352,17 +1352,17 @@ func getReparsePointForLeaf(ctx context.Context, leaf virtual.Leaf, buffer []byt return 0, nil } - // Parse the path to determine if it's absolute + // Symlink targets are stored in native (Windows) format, so + // we can pass them directly to FillSymlinkReparseBuffer. w := relativePathChecker{} if err := path.Resolve(path.LocalFormat.NewParser(string(target)), &w); err != nil { return 0, err } - var flags int + var flags uint32 if w.isRelative { - flags = windowsext.SYMLINK_FLAG_RELATIVE + flags = uint32(windowsext.SYMLINK_FLAG_RELATIVE) } - - return FillSymlinkReparseBuffer(string(target), uint32(flags), buffer) + return FillSymlinkReparseBuffer(string(target), flags, buffer) } func (fs *FileSystem) GetReparsePoint(ref *ffi.FileSystemRef, file uintptr, name string, buffer []byte) (int, error) { diff --git a/pkg/filesystem/virtual/winfsp/file_system_integration_test.go b/pkg/filesystem/virtual/winfsp/file_system_integration_test.go index 3af7006e..08405662 100644 --- a/pkg/filesystem/virtual/winfsp/file_system_integration_test.go +++ b/pkg/filesystem/virtual/winfsp/file_system_integration_test.go @@ -14,13 +14,18 @@ import ( "strings" "testing" + remoteexecution "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2" "github.com/bazelbuild/rules_go/go/runfiles" + "github.com/buildbarn/bb-remote-execution/pkg/cas" "github.com/buildbarn/bb-remote-execution/pkg/filesystem/pool" "github.com/buildbarn/bb-remote-execution/pkg/filesystem/virtual" virtual_configuration "github.com/buildbarn/bb-remote-execution/pkg/filesystem/virtual/configuration" virtual_pb "github.com/buildbarn/bb-remote-execution/pkg/proto/configuration/filesystem/virtual" "github.com/buildbarn/bb-storage/pkg/blockdevice" "github.com/buildbarn/bb-storage/pkg/clock" + "github.com/buildbarn/bb-storage/pkg/digest" + bb_filesystem "github.com/buildbarn/bb-storage/pkg/filesystem" + bb_path "github.com/buildbarn/bb-storage/pkg/filesystem/path" "github.com/buildbarn/bb-storage/pkg/program" "github.com/buildbarn/bb-storage/pkg/util" "github.com/stretchr/testify/require" @@ -39,7 +44,7 @@ func findFreeDriveLetter() (string, error) { return "", fmt.Errorf("no free drive letters available") } -func createWinFSPMountForTest(t *testing.T, terminationGroup program.Group, caseSensitive bool) (string, blockdevice.BlockDevice) { +func createWinFSPForTest(t *testing.T, terminationGroup program.Group, caseSensitive bool) (string, blockdevice.BlockDevice, virtual_configuration.Mount, virtual.PrepopulatedDirectory, virtual.SymlinkFactory) { // We can't run winfsp-tests at a directory path due to // https://github.com/winfsp/winfsp/issues/279. Instead find a free drive // letter and run it there instead. @@ -75,38 +80,42 @@ func createWinFSPMountForTest(t *testing.T, terminationGroup program.Group, case // Create a virtual directory to hold new files. defaultAttributesSetter := func(requested virtual.AttributesMask, attributes *virtual.Attributes) {} - err = mount.Expose( - terminationGroup, - virtual.NewInMemoryPrepopulatedDirectory( - virtual.NewHandleAllocatingFileAllocator( - virtual.NewPoolBackedFileAllocator( - pool.NewBlockDeviceBackedFilePool( - bd, - pool.NewBitmapSectorAllocator(uint32(sectorCount)), - sectorSizeBytes, - ), - util.DefaultErrorLogger, - defaultAttributesSetter, - virtual.NoNamedAttributesFactory, + symlinkFactory := virtual.NewHandleAllocatingSymlinkFactory( + virtual.BaseSymlinkFactory, + handleAllocator.New(), + ) + rootDir := virtual.NewInMemoryPrepopulatedDirectory( + virtual.NewHandleAllocatingFileAllocator( + virtual.NewPoolBackedFileAllocator( + pool.NewBlockDeviceBackedFilePool( + bd, + pool.NewBitmapSectorAllocator(uint32(sectorCount)), + sectorSizeBytes, ), - handleAllocator, - ), - virtual.NewHandleAllocatingSymlinkFactory( - virtual.BaseSymlinkFactory, - handleAllocator.New(), + util.DefaultErrorLogger, + defaultAttributesSetter, + virtual.NoNamedAttributesFactory, ), - util.DefaultErrorLogger, handleAllocator, - sort.Sort, - func(s string) bool { return false }, - clock.SystemClock, - normalizer, - defaultAttributesSetter, - virtual.NoNamedAttributesFactory, ), + symlinkFactory, + util.DefaultErrorLogger, + handleAllocator, + sort.Sort, + func(s string) bool { return false }, + clock.SystemClock, + normalizer, + defaultAttributesSetter, + virtual.NoNamedAttributesFactory, ) - require.NoError(t, err, "Failed to expose mount point") + return vfsPath, bd, mount, rootDir, symlinkFactory +} + +func createWinFSPMountForTest(t *testing.T, terminationGroup program.Group, caseSensitive bool) (string, blockdevice.BlockDevice) { + vfsPath, bd, mount, rootDir, _ := createWinFSPForTest(t, terminationGroup, caseSensitive) + err := mount.Expose(terminationGroup, rootDir) + require.NoError(t, err, "Failed to expose mount point") return vfsPath, bd } @@ -417,6 +426,163 @@ func TestWinFSPFileSystemGetSecurityByNameIntegration(t *testing.T) { }) } +// staticDirectoryWalker is a simple DirectoryWalker that returns a +// fixed Directory proto. +type staticDirectoryWalker struct { + directory *remoteexecution.Directory +} + +func (w *staticDirectoryWalker) GetDirectory(ctx context.Context) (*remoteexecution.Directory, error) { + return w.directory, nil +} + +func (staticDirectoryWalker) GetChild(d digest.Digest) cas.DirectoryWalker { + return &staticDirectoryWalker{} +} + +func (staticDirectoryWalker) GetDescription() string { + return "static test directory" +} + +func (staticDirectoryWalker) GetContainingDigest() digest.Digest { + return digest.MustNewDigest("test", remoteexecution.DigestFunction_SHA256, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 0) +} + +func TestWinFSPFileSystemStatFollowsSymlink(t *testing.T) { + program.RunLocal(context.Background(), func(ctx context.Context, siblingsGroup, dependenciesGroup program.Group) error { + vfsPath, bd, mount, rootDir, symlinkFactory := createWinFSPForTest(t, dependenciesGroup, false) + defer bd.Close() + + // Build a pnpm-style node_modules layout with chained + // symlinks. Symlinks are created through the CAS + // ingestion path (NewCASInitialContentsFetcher) so that + // their UNIX-formatted targets from the REv2 proto are + // normalized to native Windows format. + digestFunction := digest.MustNewFunction("test", remoteexecution.DigestFunction_SHA256) + noopFileReadMonitorFactory := virtual.FileReadMonitorFactory( + func(name bb_path.Component) virtual.FileReadMonitor { + return func() {} + }) + + // Helper to create a chain of nested directories. + mkdirs := func(parent virtual.PrepopulatedDirectory, names ...string) virtual.PrepopulatedDirectory { + for _, name := range names { + child, err := parent.CreateAndEnterPrepopulatedDirectory(bb_path.MustNewComponent(name)) + require.NoError(t, err) + parent = child + } + return parent + } + + // Helper to create symlink children via the CAS + // ingestion path, so that UNIX-formatted targets from + // the REv2 proto are normalized to native Windows format. + addCASSymlinks := func(parent virtual.PrepopulatedDirectory, symlinks []*remoteexecution.SymlinkNode) { + children, err := virtual.NewCASInitialContentsFetcher( + ctx, + &staticDirectoryWalker{directory: &remoteexecution.Directory{ + Symlinks: symlinks, + }}, + nil, symlinkFactory, digestFunction, + ).FetchContents(noopFileReadMonitorFactory) + require.NoError(t, err) + require.NoError(t, parent.CreateChildren(children, false)) + } + + // store/pkg — the real directory. + mkdirs(rootDir, "store", "pkg") + + addCASSymlinks(rootDir, []*remoteexecution.SymlinkNode{ + {Name: "store-link", Target: "store/pkg"}, + }) + + nmDir := mkdirs(rootDir, "node_modules") + innerNmDir := mkdirs(nmDir, ".pnpm", "pkg@1.0.0", "node_modules") + addCASSymlinks(innerNmDir, []*remoteexecution.SymlinkNode{ + {Name: "pkg", Target: "../../../../store/pkg"}, + }) + addCASSymlinks(nmDir, []*remoteexecution.SymlinkNode{ + {Name: "pkg", Target: ".pnpm/pkg@1.0.0/node_modules/pkg"}, + }) + + require.NoError(t, mount.Expose(dependenciesGroup, rootDir)) + + // Write a file into the real directory after mounting. + testContent := []byte(`{"name":"pkg"}`) + require.NoError(t, os.WriteFile( + filepath.Join(vfsPath, "store", "pkg", "package.json"), + testContent, 0o644, + )) + + t.Run("SingleSymlink", func(t *testing.T) { + singleSymlinkPath := filepath.Join(vfsPath, "store-link") + info, err := os.Stat(singleSymlinkPath) + require.NoError(t, err) + require.True(t, info.IsDir()) + + content, err := os.ReadFile(filepath.Join(singleSymlinkPath, "package.json")) + require.NoError(t, err) + require.Equal(t, testContent, content) + }) + + t.Run("ChainedSymlinks", func(t *testing.T) { + symlinkPath := filepath.Join(vfsPath, "node_modules", "pkg") + + info, err := os.Lstat(symlinkPath) + require.NoError(t, err) + require.NotZero(t, info.Mode()&os.ModeSymlink) + + target, err := os.Readlink(symlinkPath) + require.NoError(t, err) + require.Equal(t, `.pnpm\pkg@1.0.0\node_modules\pkg`, target) + + info, err = os.Stat(symlinkPath) + require.NoError(t, err) + require.True(t, info.IsDir()) + + content, err := os.ReadFile(filepath.Join(symlinkPath, "package.json")) + require.NoError(t, err) + require.Equal(t, testContent, content) + + entries, err := os.ReadDir(symlinkPath) + require.NoError(t, err) + require.Len(t, entries, 1) + require.Equal(t, "package.json", entries[0].Name()) + }) + + return nil + }) +} + +// This replicates what bb_runner does when it opens the WinFSP mount via +// filesystem.NewLocalDirectory and then enters subdirectories. +func TestWinFSPFileSystemCheckReadiness(t *testing.T) { + program.RunLocal(context.Background(), func(ctx context.Context, siblingsGroup, dependenciesGroup program.Group) error { + vfsPath, bd := createWinFSPMountForTest(t, dependenciesGroup, false) + defer bd.Close() + + // Create the directory structure that the build executor + // would create: /17/check_readiness + slotDir := filepath.Join(vfsPath, "17") + require.NoError(t, os.Mkdir(slotDir, 0o777)) + require.NoError(t, os.Mkdir(filepath.Join(slotDir, "check_readiness"), 0o777)) + + dir, err := bb_filesystem.NewLocalDirectory(bb_path.LocalFormat.NewParser(vfsPath + `\`)) + require.NoError(t, err, "Failed to open WinFSP mount root via NewLocalDirectory") + defer dir.Close() + + child, err := dir.EnterDirectory(bb_path.MustNewComponent("17")) + require.NoError(t, err, "Failed to enter subdirectory '17'") + defer child.Close() + + info, err := child.Lstat(bb_path.MustNewComponent("check_readiness")) + require.NoError(t, err, "Failed to stat 'check_readiness'") + require.True(t, info.Type() == bb_filesystem.FileTypeDirectory) + + return nil + }) +} + func TestWinFSPFileSystemCasePreserving(t *testing.T) { program.RunLocal(context.Background(), func(ctx context.Context, siblingsGroup, dependenciesGroup program.Group) error { vfsPath, bd := createWinFSPMountForTest(t, dependenciesGroup, false)