Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/about/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ The current members of the MkDocs-NG team.
* Fix `validation.links.not_found` is always reported as INFO for excluded pages. #32
* Fix `mkdocs serve` cleanup so temporary site directories are removed when the process receives `SIGTERM`. #36
* Fix mkdocs theme color mode switching when `highlightjs` is disabled. #39
* Fix a crash when a dangling symlink exists in the docs directory, so `mkdocs build` and `mkdocs serve` log a warning and continue instead of aborting. #43

### Maintenance

Expand Down
11 changes: 8 additions & 3 deletions mkdocs/structure/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,8 @@ def copy_file(self, dirty: bool = False) -> None:
utils.copy_file(self.abs_src_path, output_path)
except shutil.SameFileError:
pass # Let plugins write directly into site_dir.
except OSError as e:
log.warning(f"Error copying '{self.src_uri}': {e}")
elif isinstance(content, str):
with open(output_path, "w", encoding="utf-8") as output_file:
output_file.write(content)
Expand All @@ -526,9 +528,12 @@ def is_modified(self) -> bool:
return True
assert self.abs_src_path is not None
if os.path.isfile(self.abs_dest_path):
return os.path.getmtime(self.abs_dest_path) < os.path.getmtime(
self.abs_src_path
)
try:
return os.path.getmtime(self.abs_dest_path) < os.path.getmtime(
self.abs_src_path
)
except OSError:
return False
return True

def is_documentation_page(self) -> bool:
Expand Down
29 changes: 29 additions & 0 deletions mkdocs/tests/structure/file_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,35 @@ def test_copy_file_from_content(self, dest_dir):
with open(dest_path, encoding="utf-8") as f:
self.assertEqual(f.read(), "ö")

@tempdir()
@tempdir()
def test_copy_file_dangling_symlink(self, src_dir, dest_dir):
dangling = os.path.join(src_dir, "dangling.jpg")
try:
os.symlink(os.path.join(src_dir, "nonexistent"), dangling)
except (OSError, NotImplementedError):
self.skipTest("Creating symlinks not supported")

file = File("dangling.jpg", src_dir, dest_dir, use_directory_urls=False)
with self.assertLogs("mkdocs.structure.files") as cm:
file.copy_file()
self.assertRegex(
"\n".join(cm.output),
r"^WARNING:mkdocs.structure.files:Error copying 'dangling.jpg'",
)

@tempdir()
@tempdir()
def test_copy_file_missing_source(self, src_dir, dest_dir):
"""File deleted between discovery and copy should warn, not crash."""
file = File("missing.txt", src_dir, dest_dir, use_directory_urls=False)
with self.assertLogs("mkdocs.structure.files") as cm:
file.copy_file()
self.assertRegex(
"\n".join(cm.output),
r"^WARNING:mkdocs.structure.files:Error copying 'missing.txt'",
)

def test_files_append_remove_src_paths(self):
fs = [
File("index.md", "/path/to/docs", "/path/to/site", use_directory_urls=True),
Expand Down
8 changes: 2 additions & 6 deletions mkdocs/tests/utils/utils_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,15 +386,11 @@ def test_copy_files_without_permissions(self, src_dir, dst_dir):
@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.
# not crash the build. A warning should be raised 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:
with self.assertRaises(FileNotFoundError):
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")))

Expand Down
3 changes: 1 addition & 2 deletions mkdocs/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,7 @@ def copy_file(source_path: str, output_path: str) -> None:
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
raise FileNotFoundError(f"Broken symlink: {source_path}")
shutil.copyfile(source_path, output_path)


Expand Down
Loading