Skip to content

Symbolic links are resolved unnecessarily #13932

@effigies

Description

@effigies

Describe the bug

Sphinx has a number of places where it uses Path.resolve() to normalize paths. I assume this is because pathlib.Path() doesn't have any method of normalizing .. out of paths without resolving symlinks, but this can lead to problems when linking to a document section, when the target document is a symlink.

I have not seen this matter for RST-based sites, but for Markdown sites using MyST-Parser, it can lead to an issue with making explicit links between documents. For example, I might have an intro.md that links to the repository README.md. If I want to refer to a heading, I might use [Heading title](intro.md#heading-title). When that happens, I will get a warning from myst:

WARNING: Unknown source document '/path/to/project/README' [myst.xref_missing]

And the link will fail to render. I have tracked down the issue in executablebooks/MyST-Parser#1055, and the crux is that sphinx.environment.BuildEnvironment.relfn2path that they call resolves symlinks.

The following patch resolves the problem:

diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py
index 79fa62785..dfdf8bdad 100644
--- a/sphinx/environment/__init__.py
+++ b/sphinx/environment/__init__.py
@@ -438,7 +438,7 @@ def relfn2path(
         """
         file_name = Path(filename)
         if file_name.parts[:1] in {('/',), ('\\',)}:
-            abs_fn = self.srcdir.joinpath(*file_name.parts[1:]).resolve()
+            abs_fn = os.path.normpath(self.srcdir.joinpath(*file_name.parts[1:]))
         else:
             if not docname:
                 if self.docname:
@@ -447,10 +447,10 @@ def relfn2path(
                     msg = 'docname'
                     raise KeyError(msg)
             doc_dir = self.doc2path(docname, base=False).parent
-            abs_fn = self.srcdir.joinpath(doc_dir, file_name).resolve()
+            abs_fn = os.path.normpath(self.srcdir.joinpath(doc_dir, file_name))
 
         rel_fn = _relative_path(abs_fn, self.srcdir)
-        return rel_fn.as_posix(), os.fspath(abs_fn)
+        return rel_fn.as_posix(), abs_fn
 
     @property
     def found_docs(self) -> set[str]:
diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py
index 807db899a..f4138cf4b 100644
--- a/sphinx/util/osutil.py
+++ b/sphinx/util/osutil.py
@@ -178,10 +178,11 @@ def _relative_path(path: Path, root: Path, /) -> Path:
     which may happen on Windows.
     """
     # Path.relative_to() requires fully-resolved paths (no '..').
+    # Use normpath to avoid resolving symlinks. There is no pathlib equivalent.
     if '..' in path.parts:
-        path = path.resolve()
+        path = Path(os.path.normpath(path))
     if '..' in root.parts:
-        root = root.resolve()
+        root = Path(os.path.normpath(root))
 
     if path.anchor != root.anchor or '..' in root.parts:
         # If the drives are different, no relative path exists.

Now, this case does not present in RST because RST does not have equivalent link form:

`link text <relpath.rst#section>`_

I would be happy to submit this patch (or another that fits Sphinx's conventions better) if you'd be interested.

How to Reproduce

mkdir docs
cat <<EOF > README.md
# Header

## Section

Text
EOF

uv init --bare --name=reproduction --no-workspace
uv add sphinx myst-parser
uv run sphinx-quickstart --project="Reproduction" --author="Author Name" \
        --no-sep -r '' -l en --extensions myst_parser \
        docs/

ln -s ../README.md docs/description.md

cat <<EOF >> docs/conf.py

myst_heading_anchors = 2
EOF

cat <<EOF > docs/document.md
# Document Title

Explicitly linking to [Section](description.md#section).
EOF

cat <<EOF >> docs/index.rst
   description.md
   document.md
EOF

uv run sphinx-build -b html docs docs/_build/html

Environment Information

Platform:              linux; (Linux-6.16.7-200.fc42.x86_64-x86_64-with-glibc2.41)
Python version:        3.13.7 (main, Sep  2 2025, 14:21:46) [Clang 20.1.4 ])
Python implementation: CPython
Sphinx version:        8.2.3
Docutils version:      0.21.2
Jinja2 version:        3.1.6
Pygments version:      2.19.2

Sphinx extensions

myst-parser

Additional context

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions