diff --git a/CHANGELOG.md b/CHANGELOG.md index 592be6a4e0..67608ad289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Version [v1.2.2] - 2024-01-15 + +### Fixed + +* Fix the handling of file paths in source URL generation, to fix standard library links in the Julia manual. ([#2372]) + ## Version [v1.2.1] - 2023-12-02 ### Fixed @@ -1297,6 +1303,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [v1.1.2]: https://github.com/JuliaDocs/Documenter.jl/releases/tag/v1.1.2 [v1.2.0]: https://github.com/JuliaDocs/Documenter.jl/releases/tag/v1.2.0 [v1.2.1]: https://github.com/JuliaDocs/Documenter.jl/releases/tag/v1.2.1 +[v1.2.2]: https://github.com/JuliaDocs/Documenter.jl/releases/tag/v1.2.2 [#198]: https://github.com/JuliaDocs/Documenter.jl/issues/198 [#245]: https://github.com/JuliaDocs/Documenter.jl/issues/245 [#487]: https://github.com/JuliaDocs/Documenter.jl/issues/487 @@ -1765,6 +1772,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#2348]: https://github.com/JuliaDocs/Documenter.jl/issues/2348 [#2364]: https://github.com/JuliaDocs/Documenter.jl/issues/2364 [#2365]: https://github.com/JuliaDocs/Documenter.jl/issues/2365 +[#2372]: https://github.com/JuliaDocs/Documenter.jl/issues/2372 [JuliaLang/julia#36953]: https://github.com/JuliaLang/julia/issues/36953 [JuliaLang/julia#38054]: https://github.com/JuliaLang/julia/issues/38054 [JuliaLang/julia#39841]: https://github.com/JuliaLang/julia/issues/39841 diff --git a/Project.toml b/Project.toml index 7ef4d50ceb..949b712543 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "Documenter" uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -version = "1.2.1" +version = "1.2.2" [deps] ANSIColoredPrinters = "a4c015fc-c6ff-483c-b24f-f7ea428134e9" diff --git a/src/documents.jl b/src/documents.jl index 33e72ce035..04b4f9a733 100644 --- a/src/documents.jl +++ b/src/documents.jl @@ -281,7 +281,7 @@ struct RemoteRepository end function RemoteRepository(root::AbstractString, remote::Remotes.Remote) try - RemoteRepository(realpath(root), remote, repo_commit(root)) + RemoteRepository(normpath(root), remote, repo_commit(root)) catch e e isa RepoCommitError || rethrow() @error "Unable to determine the commit for the remote repository:\n$(e.msg)" e.directory exception = e.err_bt @@ -507,7 +507,7 @@ function interpret_repo_and_remotes(; root, repo, remotes) if !isdir(path) throw(ArgumentError(("Invalid local path in remotes (not a directory): $(path)"))) end - path = realpath(path) + path = normpath(path) # We'll also check that there are no duplicate entries. idx = findfirst(isequal(path), [remote.root for remote in remotes_checked]) if !isnothing(idx) @@ -788,9 +788,30 @@ function relpath_from_remote_root(doc::Document, path::AbstractString) end ispath(path) || error("relpath_from_repo_root called with nonexistent path: $path") isabspath(path) || error("relpath_from_repo_root called with non-absolute path: $path") - # We want to expand the path properly, including symlinks, so we call realpath() - # Note: it throws for non-existing files, but we just checked for it. - path = realpath(path) + # We'll normalize the path, removing `..`-s etc. + # + # However, we explicitly do not want to resolve symlinks (so we can't call `realpath`). + # This is, in particular, necessary to enable us to resolve standard library paths for + # the Julia manual -- the standard library source files under usr/share/julia are _sometimes_ + # symlinked to stdlib/ and sometimes they are not. + # + # Specifically, the (fixed; i.e. `Base.fixup_stdlib_path` has been called) paths of standard + # library docstrings always point into + # + # $(JULIA_SOURCE)/usr/share/julia/stdlib/v1.11/$(PACKAGE)/src/... + # + # If this is a Julia worktree that has been created by unpacking a pre-built tarball (like we + # do in the Julia documentation builds on CI), then those are real files. However, if you are + # building the documentation in a full local Julia worktree, then the standard library package + # directories are actually symlinks into $(JULIA_SOURCE)/stdlib. This would throw `realpath` + # off. + # + # For the normal case of building documentation for Julia packages, we do not expect to have to + # deal with symlinks at all, unless it's a very strange setup. However, even in that case, it + # feels safer to _not_ expand symlinks, since the symlinks might be used to organize directories + # at a higher level (e.g. closer to the root; link symlinking /home directories or something + # strange like that). + path = normpath(path) # Try to see if `path` falls into any of the remotes in .remotes, or if it's a GitHub repository # we can automatically "configure". root_remote::Union{RemoteRepository,Nothing} = nothing @@ -889,7 +910,17 @@ function source_url(doc::Document, mod::Module, file::AbstractString, linerange) return repofile(julia_remote, ref, "base/$file", linerange) end # Generally, we assume that the Julia source file exists on the system. - isfile(file) || return nothing + # An exception to this is when the path points to a docstring in a standard library. + # To handle that case, we first "fix up" the path, hopefully making it point to the + # local usr/share/julia directory. + if !isfile(file) + # Note: Base.fixup_stdlib_path is a non-public internal function. + file = Base.fixup_stdlib_path(file) + end + if !isfile(file) + @warn "Trying to generate a source URL for a non-existent file" mod file linerange + return nothing + end remoteref = relpath_from_remote_root(doc, file) if isnothing(remoteref) throw(MissingRemoteError(; path = file, linerange, mod)) diff --git a/src/utilities/utilities.jl b/src/utilities/utilities.jl index 1dc397e2ee..4a2dc67cd5 100644 --- a/src/utilities/utilities.jl +++ b/src/utilities/utilities.jl @@ -390,14 +390,10 @@ If `path` is a directory, it may itself already be a root. The predicate `f` gets called with absolute paths to directories and must return `true` if the directory is a "root". An example predicate is `is_git_repo_root` that checks if the directory is a Git repository root. - -The `dbdir` keyword argument specifies the name of the directory we are searching for to -determine if this is a repository or not. If there is a file called `dbdir`, then it's -contents is checked under the assumption that it is a Git worktree or a submodule. """ function find_root_parent(f, path) ispath(path) || throw(ArgumentError("find_root_parent called with non-existent path\n path: $path")) - path = realpath(path) + path = normpath(path) parent_dir = isdir(path) ? path : dirname(path) parent_dir_last = "" while parent_dir != parent_dir_last diff --git a/test/base_assumptions.jl b/test/base_assumptions.jl new file mode 100644 index 0000000000..820234ca37 --- /dev/null +++ b/test/base_assumptions.jl @@ -0,0 +1,17 @@ +""" +These tests test assumptions about the Julia Base module. In particular, this will +test the presence of the various internal, non-public functions that are used by +Documenter.jl. +""" +module BaseAssumptionTests +using Test + +@testset "Julia Base assumptions" begin + # To handle source URLs to standard library files, we need to fix up the paths to + # standard library objects (which generally point to /cache/..., for the pre-built + # binaries). + @test isdefined(Base, :fixup_stdlib_path) + @test hasmethod(Base.fixup_stdlib_path, (String,)) +end + +end diff --git a/test/runtests.jl b/test/runtests.jl index 58240e2b8e..7da940ca50 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -30,6 +30,7 @@ end @quietly include("plugins/make.jl") # Unit tests for module internals. + include("base_assumptions.jl") include("except.jl") include("utilities.jl") diff --git a/test/utilities.jl b/test/utilities.jl index d9a9d793a7..6f353c80c6 100644 --- a/test/utilities.jl +++ b/test/utilities.jl @@ -209,7 +209,7 @@ end @test Documenter.source_url(doc, Documenter, filepath, 10:20) == "//blob/$(commit)/src/SourceFile.jl#L10-L20" # repo_root & relpath_from_repo_root - @test repo_root(filepath) == realpath(joinpath(dirname(filepath), "..")) + @test repo_root(filepath) == rstrip(normpath(joinpath(dirname(filepath), "..")), ['\\', '/']) # normpath() leaves a trailing / @test repo_root(filepath, ".svn") === nothing let remoteref = Documenter.relpath_from_remote_root(doc, filepath) @test remoteref.repo.remote === remote @@ -235,7 +235,7 @@ end @test Documenter.source_url(doc, Documenter, filepath, 10:20) == "//blob/$(commit)/src/SourceFile.jl#L10-L20" # repo_root & relpath_from_repo_root - @test repo_root(filepath) == realpath(joinpath(dirname(filepath), "..")) + @test repo_root(filepath) == rstrip(normpath(joinpath(dirname(filepath), "..")), ['\\', '/']) # normpath() leaves a trailing / @test repo_root(filepath, ".svn") === nothing let remoteref = Documenter.relpath_from_remote_root(doc, filepath) @test remoteref.repo.remote === remote @@ -278,7 +278,7 @@ end @test Documenter.source_url(doc, Documenter, filepath, 10:20) == "//blob/$(commit)/src/SourceFile.jl#L10-L20" # repo_root & relpath_from_repo_root - @test repo_root(filepath) == realpath(joinpath(dirname(filepath), "..")) + @test repo_root(filepath) == rstrip(normpath(joinpath(dirname(filepath), "..")), ['\\', '/']) # normpath() leaves a trailing / @test repo_root(filepath, ".svn") === nothing let remoteref = Documenter.relpath_from_remote_root(doc, filepath) @test remoteref.repo.remote === remote