diff --git a/cli/internal/daemonclient/daemonclient.go b/cli/internal/daemonclient/daemonclient.go index 262d11e251ab5..23a3fb53481f1 100644 --- a/cli/internal/daemonclient/daemonclient.go +++ b/cli/internal/daemonclient/daemonclient.go @@ -5,6 +5,8 @@ package daemonclient import ( "context" "path/filepath" + "runtime" + "strings" "github.com/vercel/turbo/cli/internal/daemon/connector" "github.com/vercel/turbo/cli/internal/fs/hash" @@ -32,12 +34,32 @@ func New(client *connector.Client) *DaemonClient { } } +// formats a repo-relative glob to unix format with ':' characters handled. +// On windows, ':' is an invalid path character, but you can, and Turborepo does, +// read to and write from files that contain alternate data streams denoted by ':'. +// In the case of windows and an alternate data stream, we want change notifications just +// for the root file. Note that since ':' denotes a data stream for a _file_, it cannot +// appear in a directory name. Thus, if we find one, we know it's in the filename. +// See https://learn.microsoft.com/en-us/sysinternals/downloads/streams +func formatRepoRelativeGlob(input string) string { + unixInput := filepath.ToSlash(input) + if runtime.GOOS == "windows" { + colonIndex := strings.Index(input, ":") + if colonIndex > -1 { + // we found an alternate data stream + unixInput = unixInput[:colonIndex] + } + return unixInput + } + return strings.ReplaceAll(unixInput, ":", "\\:") +} + // GetChangedOutputs implements runcache.OutputWatcher.GetChangedOutputs func (d *DaemonClient) GetChangedOutputs(ctx context.Context, hash string, repoRelativeOutputGlobs []string) ([]string, int, error) { // The daemon expects globs to be unix paths var outputGlobs []string for _, outputGlob := range repoRelativeOutputGlobs { - outputGlobs = append(outputGlobs, filepath.ToSlash(outputGlob)) + outputGlobs = append(outputGlobs, formatRepoRelativeGlob(outputGlob)) } resp, err := d.client.GetChangedOutputs(ctx, &turbodprotocol.GetChangedOutputsRequest{ Hash: hash, @@ -55,10 +77,10 @@ func (d *DaemonClient) NotifyOutputsWritten(ctx context.Context, hash string, re var inclusions []string var exclusions []string for _, inclusion := range repoRelativeOutputGlobs.Inclusions { - inclusions = append(inclusions, filepath.ToSlash(inclusion)) + inclusions = append(inclusions, formatRepoRelativeGlob(inclusion)) } for _, exclusion := range repoRelativeOutputGlobs.Exclusions { - exclusions = append(exclusions, filepath.ToSlash(exclusion)) + exclusions = append(exclusions, formatRepoRelativeGlob(exclusion)) } _, err := d.client.NotifyOutputsWritten(ctx, &turbodprotocol.NotifyOutputsWrittenRequest{ Hash: hash, diff --git a/cli/internal/daemonclient/daemonclient_test.go b/cli/internal/daemonclient/daemonclient_test.go new file mode 100644 index 0000000000000..634f0bfc79c94 --- /dev/null +++ b/cli/internal/daemonclient/daemonclient_test.go @@ -0,0 +1,23 @@ +package daemonclient + +import ( + "path/filepath" + "runtime" + "testing" +) + +func TestFormatRepoRelativeGlob(t *testing.T) { + rawGlob := filepath.Join("some", ".turbo", "turbo-foo:bar.log") + // Note that we expect unix slashes whether or not we are on Windows + var expected string + if runtime.GOOS == "windows" { + expected = "some/.turbo/turbo-foo" + } else { + expected = "some/.turbo/turbo-foo\\:bar.log" + } + + result := formatRepoRelativeGlob(rawGlob) + if result != expected { + t.Errorf("formatRepoRelativeGlob(%v) got %v, want %v", rawGlob, result, expected) + } +} diff --git a/cli/internal/runcache/runcache.go b/cli/internal/runcache/runcache.go index 2abf4f8af4600..9e371cffeeb06 100644 --- a/cli/internal/runcache/runcache.go +++ b/cli/internal/runcache/runcache.go @@ -123,7 +123,6 @@ func (tc *TaskCache) RestoreOutputs(ctx context.Context, prefixedUI *cli.Prefixe if err != nil { progressLogger.Warn(fmt.Sprintf("Failed to check if we can skip restoring outputs for %v: %v. Proceeding to check cache", tc.pt.TaskID, err)) - prefixedUI.Warn(ui.Dim(fmt.Sprintf("Failed to check if we can skip restoring outputs for %v: %v. Proceeding to check cache", tc.pt.TaskID, err))) changedOutputGlobs = tc.repoRelativeGlobs.Inclusions } diff --git a/crates/turborepo-filewatch/src/globwatcher.rs b/crates/turborepo-filewatch/src/globwatcher.rs index 2a64ae924bb82..fb05222813df5 100644 --- a/crates/turborepo-filewatch/src/globwatcher.rs +++ b/crates/turborepo-filewatch/src/globwatcher.rs @@ -593,7 +593,12 @@ mod test { let glob_watcher = GlobWatcher::new(&repo_root, cookie_jar, watcher.subscribe()); + // On windows, we expect different sanitization before the + // globs are passed in, due to alternative data streams in files. + #[cfg(windows)] let raw_includes = &["my-pkg/.next/next-file"]; + #[cfg(not(windows))] + let raw_includes = &["my-pkg/.next/next-file\\:build"]; let raw_excludes: [&str; 0] = []; let globs = GlobSet { include: make_includes(raw_includes), @@ -618,10 +623,8 @@ mod test { assert!(results.is_empty()); // Change the watched file - repo_root - .join_components(&["my-pkg", ".next", "next-file"]) - .create_with_contents("hello") - .unwrap(); + let watched_file = repo_root.join_components(&["my-pkg", ".next", "next-file:build"]); + watched_file.create_with_contents("hello").unwrap(); let results = glob_watcher .get_changed_globs(hash.clone(), candidates.clone()) .await