Skip to content

Treat Windows junctions as symlinks in Realpath #186

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

Merged
merged 12 commits into from
Jan 15, 2025

Conversation

jakebailey
Copy link
Member

@jakebailey jakebailey commented Dec 17, 2024

For a long time, Windows did not have real symlinks (now available in dev mode), so effectively every tool (e.g., pnpm) uses junctions instead.

CL 565136 (released in Go 1.23) changed Go's handling of Windows junctions such that they are no longer treated as symlinks, but as irregular files. This breaks Realpath in our code because we want to treat junctions as symlinks, which is how Node behaves.

With pnpm on a path like D:/project/node_modules/package/index.d.ts, a Realpath implementation might do something like this:

  1. Start with D:/project/node_modules/package/index.d.ts
  2. D:/project is not a symlink, but is a dir. Continue.
  3. D:/project/node_modules is not a symlink, but is a dir. Continue.
  4. D:/project/node_modules/package is a symlink to D:/project/node_modules/.pnpm/[email protected]/node_modules/package.
    • Start over with D:/project/node_modules/.pnpm/[email protected]/node_modules/package/index.d.ts
  5. D:/project is not a symlink, but is a dir. Continue.
  6. D:/project/node_modules is not a symlink, but is a dir. Continue.
  7. D:/project/node_modules/.pnpm is not a symlink, but is a dir. Continue.
  8. D:/project/node_modules/.pnpm/[email protected] is not a symlink, but is a dir. Continue.
  9. D:/project/node_modules/.pnpm/[email protected]/node_modules is not a symlink, but is a dir. Continue.
  10. D:/project/node_modules/.pnpm/[email protected]/node_modules/package is not a symlink, but is a dir. Continue.
  11. D:/project/node_modules/.pnpm/[email protected]/node_modules/package/index.d.ts is not a symlink, nor is it a dir, but we're at the end.
    • The entire path has been traversed; this is our "real path".

CL 565136 throws a wrench in things in that at step 4, it's not a symlink. But, since it's also not a dir, but an irregular file, the process bails because it doesn't make sense that there's a non-directory thing in the middle of a path.

This PR copies the implementation of EvalSymlinks, modifying it slightly so tat on Windows, we treat a junction like a symlink. The rest of the code works fine after that.

Another option is to set GODEBUG=winsymlink=0 (in env at build, or maybe in go.mod?), which restores the pre Go-1.23 behavior. But, I'm moderately wary of setting that at startup, or using it at all lest it be removed.

I've instead looked for another way to do this via the Windows API, emulating what libuv and other packages like go-winio and the stdlib do; the PR now does that instead.

This deserves a test, but, that would require me knowing how to programmatically make a junction so that will take some more effort to figure out. Wrote a test.

@jakebailey jakebailey changed the title Copy Go implementation of EvalSymlinks Treat Windows junctions as symlinks in Realpath Dec 17, 2024
@jakebailey
Copy link
Member Author

It may also be the case that we need more of this for other things, specifically os.DirFS, because based on discussions on the linked CL and related issues, file walking is apparently not supposed to follow junctions (reparse points etc) either. But, we really need it to. The same goes for stat calls, readdir, etc....

I believe we use Realpath already in the places we do manual directory walks (module resolution)? But, for include/exclude/files, I am not sure what we do there. Presumably, our implementation in the old TS compiler was able to prevent infinite recursion. It's likely that if we ditch WalkDir for program load (what we're probably going to do anyway), then everything else may be okay.

@jakebailey jakebailey force-pushed the jabaile/fix-windows-junctions branch from de16b2e to 8998046 Compare December 18, 2024 17:13
@jakebailey
Copy link
Member Author

I've updated the PR to instead use the Windows API GetFinalPathNameByHandle to implement realpath; this is what libuv does under the hood: https://github.com/libuv/libuv/blob/ec5a4b54f7da7eeb01679005c615fee9633cdb3b/src/win/fs.c#L2937

It's also what go-winio does: https://github.com/microsoft/go-winio/blob/3c9576c9346a1892dee136329e7e15309e82fb4f/pkg/fs/resolve.go#L85

But, we want the DOS path, not some sort of GUID volume path thingy, which is what importing the above would do, so I had to do it myself.

@jakebailey
Copy link
Member Author

Long paths strike again:

    realpath_test.go:49: assertion failed: C:/Users/RUNNER~1/AppData/Local/Temp/TestSymlinkRealpath2411123161/001/target/file (targetRealpath string) != C:/Users/runneradmin/AppData/Local/Temp/TestSymlinkRealpath2411123161/001/target/file (linkRealpath string)

It seems like I'll need to go back to the non-syscall version...

@jakebailey
Copy link
Member Author

Oddly, this implies that the temp path I'm starting with is the RUNNER~1, and the new code is returning the "good" path.

But, I added something which calls out to node, and:

--- FAIL: TestSymlinkRealpath (0.14s)
    realpath_test.go:51: assertion failed: C:/Users/RUNNER~1/AppData/Local/Temp/TestSymlinkRealpath3387900004/001/target/file (targetRealpath string) != C:/Users/runneradmin/AppData/Local/Temp/TestSymlinkRealpath3387900004/001/target/file (linkRealpath string)
    realpath_test.go:55: node: {
          native: 'C:\\Users\\runneradmin\\AppData\\Local\\Temp\\TestSymlinkRealpath[33](https://github.com/microsoft/typescript-go/actions/runs/12418471234/job/34671739058?pr=186#step:8:34)87900004\\001\\target\\file',
          node: 'C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\TestSymlinkRealpath3387900004\\001\\target\\file'
        }

So, using the non-native resolver does the same as the old Go code, but it's also not less than 260 characters, so TS proper would have seen the native one?

@jakebailey jakebailey force-pushed the jabaile/fix-windows-junctions branch from 37dd04a to 933f112 Compare December 19, 2024 20:19
@jakebailey
Copy link
Member Author

I've reverted back to the EvalSymlinks replacement, which passes tests. It's not entirely clear to me what the right answer is (the syscall or not), but worse is to do nothing, so I would like to get this merged in some form.

@jakebailey
Copy link
Member Author

I'm actually going to revert it back again to the syscall based version; that's what Node is doing, and I think the actual difference here is that the tempdir given to us by Go is actually not a realpath itself.

Comment on lines +58 to +66
_FILE_ANY_ACCESS = 0

_FILE_SHARE_READ = 0x01
_FILE_SHARE_WRITE = 0x02
_FILE_SHARE_DELETE = 0x04

_OPEN_EXISTING = 0x03

_FILE_FLAG_BACKUP_SEMANTICS = 0x0200_0000
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the naming convention used within the Go stdlib for Windows code that uses constants not defined elsewhere.

@jakebailey jakebailey merged commit 1030abf into main Jan 15, 2025
14 checks passed
@jakebailey jakebailey deleted the jabaile/fix-windows-junctions branch January 15, 2025 19:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants