Add os.junction & pathlib.Path.junction_to

You can’t create symlinks on Windows if developer mode is not enabled and you’re not running your program as administrator, but junctions don’t have these restrictions.

Could it be useful to add a function to create junctions, which could create a symlink on posix matching the conditions for junctions? Here’s an example implementation, but ideally this should be achieved without subprocess:

import os
import subprocess
from os.path import isabs, isdir

if os.name == "nt":
    def junction(src, dst):
        """Make junction."""
        if not isabs(src):
            raise ValueError("source must be absolute")

        if not isdir(src):
            raise ValueError("source must be a directory")

        cmd = ['mklink', '/j', os.fsdecode(dst), os.fsdecode(src)]
        proc = subprocess.run(cmd, shell=True, capture_output=True)
        if proc.returncode:
            raise OSError(proc.stderr.decode().strip())
else:
    def junction(src, dst):
        """Make junction."""
        if not isabs(src):
            raise ValueError("source must be absolute")

        if not isdir(src):
            raise ValueError("source must be a directory")

        os.symlink(src, dst)

Correction: you can use _winapi.CreateJunction() & the check should be on src not dst.

The system has to be in developer mode in order to allow unprivileged symlink creation. Alternatively, an administrator can assign the symlink privilege to an unrestricted group (e.g. “Users” or “Authenticated Users”) or directly to an account. At logon, the symlink privilege only gets filtered from the privilege sets of UAC restricted groups, such as “Administrators”, not from the privilege sets that are assigned to unrestricted groups or accounts.

Note that mountpoints (junctions) are only allowed to target a directory in a local filesystem. This is required because a mountpoint in a remote filesystem gets evaluated on the server side, unlike a symlink, which gets evaluated on the client side. On the plus side, this design allows linking to local filesystems on a server without having to expose them as network shares.

_winapi.CreateJunction() was implemented only for internal testing. It is not ready for real-world use. No one should be using it for anything other than testing. In a comment on issue gh-97586, I demonstrated a C implementation of a more robust CreateJunction(). It would also need a comprehensive set of tests.

1 Like

_winapi.CreateJunction() was implemented only for internal testing. It is not ready for real-world use. No one should be using it for anything other than testing.

OK, I’ll update the example to use subprocess.run(['mklink', '/j', dst, src], shell=True) again.

Note that mountpoints (junctions) are only allowed to target a directory in a local filesystem. This is required because a mountpoint in a remote filesystem gets evaluated on the server side, unlike a symlink, which gets evaluated on the client side. On the plus side, this design allows linking to local filesystems on a server without having to expose them as network shares.

How should I check if a directory is on a remote file system?

I don’t think so, not as a stdlib function. The usefulness is pretty limited.

I suggest you collect your various ideas for filesystem functions (this one, plus the “is hidden” and “has data” suggestions) and create a library on PyPI that provides them. If that library gets a lot of interest and use, then that would provide some evidence that the functions might be worth adding to the stdlib at some poin. And if not, then at least you’ve created a useful resource for those people who do have a need for them. And you can use them for yourself just as easily whether they are included in your application or in a separate library[1].

If you need help implementing these functions in your library, you should ask on the “Python Help” category, rather than here.


  1. I assume you do plan on writing and using the functions for your own purposes, regardless of the outcome of this discussion, otherwise you’re wasting everyone’s time proposing ideas no-one has expressed an interest in ↩︎

9 Likes

The system doesn’t validate the target path when setting the reparse-point data of a mountpoint, so setting a remote path or nonexistent path as the target will succeed. However, trying to traverse a mountpoint will fail if it’s a remote path.

The mklink /j implementation in CMD tests for the local filesystem requirement before creating a mountpoint. For both the target path and the junction’s parent directory, it calls WinAPI GetVolumePathNameW() to get the volume root directory, and it checks for a remote filesystem (i.e. DRIVE_REMOTE) via WinAPI GetDriveTypeW(). For the junction path, it also checks for an NTFS or ReFS filesystem via WinAPI GetVolumeInformationW(), but this step isn’t necessary; other filesystem types can support reparse points. The GetVolumePathNameW() step also isn’t necessary, though I kept it in my demo implementation. It’s actually buggy for substitute drives (e.g. subst W: C:\Windows). In practice, GetDriveTypeW() works for any directory, not just the root directory, as long as the path ends with a backslash. It even works for a file if the path ends with a backslash (weird). The target path should be checked to ensure it’s a directory via WinAPI GetFileAttributesW().

1 Like

Thanks, I was already starting to think it didn’t check anything. I updated the command to raise an error in that case:

cmd = ['mklink', '/j', os.fsdecode(dst), os.fsdecode(src)]
proc = subprocess.run(cmd, shell=True, capture_output=True)
if proc.returncode:
    raise OSError(proc.stderr.decode().strip())

Speaking as someone that occasionally uses junctions on Windows - to achieve what appears to me to be the same result that I would get from a symlink on non-Windows - without understanding that much of how Windows junctions actually work internally:

I think it’d be useful to have this functionality available, but I would prefer a bool kwarg junction (or make_junction or use_junction or similar) in the existing os.symlink and Path.symlink_to functions, rather than creating new functions. On non-Windows, it would ignore the kwarg (rather than raise), so that if you wanted your code to “make a junction on Windows or a symlink on non-Windows”, you could just write p.symlink_to(q, junction=True).

If there’s some crucial reason I’m missing why we definitely shouldn’t consider Windows junctions to be more-or-less equivalent to non-Windows symlinks, I’d certainly like to learn what this is.

1 Like

Isn’t that simply because the functionality of junctions itself is limited, and you can get symlinks working using developer mode?

Still, a program that wants to use them is currently forced to use subprocess, or a function that’s probably more error prone than the function used for internal testing.

Without junction, I don’t even see the value in being able to distinguish symlinks from junctions: when you want to edit them you’ll have to use a symlink regardless.

Junctions are mount points (i.e. IO_REPARSE_TAG_MOUNT_POINT) for directories in local filesystems. Python classifies them as directories (i.e. S_IFDIR file type), not as POSIX symlinks (i.e. S_IFLNK file type).

Currently there’s no ability to preserve junctions in common code that copies links via os.symlink(os.readlink(src), dst). This is because os.readlink() just returns the target path without indicating whether it’s a symlink or junction, and os.symlink() can only create symlinks.

One important difference between junctions and symlinks is that junctions in remote paths are resolved on the server side using the server’s local devices. That’s why junctions are only allowed to target directories in local filesystems. For example, in a remote path “\\server\share\junction”, the junction might target “E:\dir” on the server’s “E:” drive. If you have a similar symlink “\\server\share\symlink” that also targets “E:\dir”, then, assuming it’s allowed, the path instead gets evaluated on the client’s “E:” drive, which may or may not exist. Because this is extremely dangerous and insane behavior, by default it is forbidden by the remote-to-local (R2L) symlink evaluation policy on client machines, in which case trying to traverse the symlink will fail.

There also used to be an important difference in how the kernel handled symlinks versus junctions when resolving the target path of a relative symlink. Say a symlink has a relative target path “spam\..\file”. It used to be that if “spam” in this target path was itself a symlink, that its target path would be evaluated first before resolving the “..” component. The “spam” symlink could resolve to a directory at any relative or absolute path, possibly on another drive or share. That’s the expected POSIX style physical evaluation of the path. OTOH, if “spam” was a junction (i.e. a mount point), the “..” component was logically resolved, just as it would be if “spam” were a regular directory, and thus the relative target path would resolve as just “file”. However, this POSIX-like behavior in the kernel was inconsistent with how the Windows API logically normalizes “..” components, irrespective of symlinks, when it opens a path. An update in Windows 11 addressed the inconsistency in the kernel by logically normalizing the target path of a relative symlink before evaluating it. Thus including “..” components in the target path of a relative symlink is now completely pointless on Windows. The target path may as well be logically normalized before creating the symlink.

2 Likes

No, it’s not. In my experience there’s very little demand for such a function, it’s as simple as that. I’ve never seen anyone ask for such a function, and I’ve never seen a project implement it as an internal utility.

As I said, write a 3rd party module implementing this function (and your other proposals) if you want to establish that there’s a demand for it. But please stop wasting people’s time by proposing speculative arguments without backing them up with any evidence.

3 Likes

OK, then not.

I think a package just for creating junctions is unlikely to be used, instead of simply using the CMD shell’s mklink command. That doesn’t mean it wouldn’t be useful if it existed. A while ago Steve Dower mentioned to me that he wanted to create a filesystem package that would support Windows-specific filesystem features, such as filesystem security and exposing placeholder reparse points. I don’t know what came of that idea. A package like that is where I’d expect that a function to create junctions would fine a useful home.

Junction mount points are a fundamental file type on Windows. The system uses them for volume mount points – i.e. junctions that are registered by the mount-point manager, which target the root directory of a volume using its system-generated, persistent “Volume{GUID}” name. In general they’re like bind mounts on Unix systems.

A convenient use for junctions is linking to directories in local filesystems from within a tree that’s accessed as an SMB share. Doing the same with a symlink requires creating another share and linking to a UNC path, which has to be evaluated on the client side. By default the symlink evaluation policy disallows evaluating remote-to-remote (R2R) symlinks (by default only L2L and L2R symlinks are allowed), so all accessing clients have to be specially configured to allow this.

3 Likes