diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index 06da0671..c95fe226 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -27,6 +27,7 @@ The current members of the MkDocs-NG team. ### Fixed +* Fix build crash caused by broken (dangling) symlinks in the docs directory. #46 * Fix malformed URLs (e.g., unterminated IPv6 literals) crashing the entire build. #45 ## Version 1.7.1 (2026-04-25) diff --git a/mkdocs/tests/utils/utils_tests.py b/mkdocs/tests/utils/utils_tests.py index df5fd678..940902a8 100644 --- a/mkdocs/tests/utils/utils_tests.py +++ b/mkdocs/tests/utils/utils_tests.py @@ -382,6 +382,22 @@ def test_copy_files_without_permissions(self, src_dir, dst_dir): if os.path.exists(src): os.chmod(src, stat.S_IRUSR | stat.S_IWUSR) + @tempdir() + @tempdir() + def test_copy_broken_symlink(self, src_dir, dst_dir): + # Regression test for mkdocs/mkdocs#3785: dangling symlinks should + # not crash the build. A warning should be logged and the file skipped. + broken_link = os.path.join(src_dir, "broken_link") + os.symlink("/nonexistent/path", broken_link) + with self.assertLogs("mkdocs", level="WARNING") as cm: + utils.copy_file(broken_link, os.path.join(dst_dir, "broken_link")) + self.assertTrue( + any("Symlink broken" in m for m in cm.output), + f"Expected a warning about broken symlink, got: {cm.output}", + ) + # The broken symlink should not have been copied. + self.assertFalse(os.path.exists(os.path.join(dst_dir, "broken_link"))) + def test_mm_meta_data(self): doc = dedent( """ diff --git a/mkdocs/utils/__init__.py b/mkdocs/utils/__init__.py index 2cfdc0a1..5e56b5e9 100644 --- a/mkdocs/utils/__init__.py +++ b/mkdocs/utils/__init__.py @@ -121,6 +121,9 @@ def copy_file(source_path: str, output_path: str) -> None: os.makedirs(output_dir, exist_ok=True) if os.path.isdir(output_path): output_path = os.path.join(output_path, os.path.basename(source_path)) + if os.path.islink(source_path) and not os.path.exists(os.readlink(source_path)): + log.warning(f"Symlink broken, could not copy file: {source_path}") + return shutil.copyfile(source_path, output_path)