Skip to content

Commit 78028db

Browse files
authored
fix(packagebundler): filter out dangling symlinks (#143)
`cp` errors out in libuv when it encounters dangling symlinks and `follow_symlinks` is set to `true`. In this PR, instead of calling `cp` directly on the directory we walk the directory tree copying files one by one and ignoring dangling symlinks.
1 parent 6f39c1b commit 78028db

4 files changed

Lines changed: 60 additions & 1 deletion

File tree

docs/src/internal.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
```@docs
66
JuliaHub._PackageBundler.bundle
77
JuliaHub._PackageBundler.path_filterer
8+
JuliaHub._PackageBundler.cp_skip_dangling_symlinks
89
```
910

1011
## Index

src/PackageBundler/PackageBundler.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ function bundle(dir; output="", force=false, allownoenv=false, verbose=true)::St
9696
# add the depot and such, and finally tar all that up.
9797
tmp_dir = mktempdir()
9898
output_dir = joinpath(tmp_dir, name)
99-
cp(dir, output_dir; follow_symlinks=true)
99+
cp_skip_dangling_symlinks(dir, output_dir)
100100
bundle_dir = joinpath(output_dir, ".bundle")
101101
mkpath(bundle_dir)
102102
# Bundle artifacts

src/PackageBundler/utils.jl

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,30 @@ function get_bundleignore(file, top)
9696
return patterns, top
9797
end
9898

99+
"""
100+
cp_skip_dangling_symlinks(src, dst)
101+
102+
Recursively copies the directory `src` to `dst`, mirroring the behaviour of
103+
`cp(src, dst; follow_symlinks=true)` but silently skipping any dangling symlinks
104+
(symlinks whose target does not exist) instead of erroring on them.
105+
"""
106+
function cp_skip_dangling_symlinks(src::AbstractString, dst::AbstractString)
107+
mkpath(dst)
108+
for entry in readdir(src)
109+
src_entry = joinpath(src, entry)
110+
dst_entry = joinpath(dst, entry)
111+
if isdir(src_entry)
112+
cp_skip_dangling_symlinks(src_entry, dst_entry)
113+
elseif isfile(src_entry)
114+
cp(src_entry, dst_entry; follow_symlinks=true)
115+
elseif islink(src_entry)
116+
# Dangling symlink: isfile/isdir follow symlinks so both return false for a
117+
# dangling symlink, while islink uses lstat so it returns true.
118+
@warn "Skipping dangling symlink" path = src_entry
119+
end
120+
end
121+
end
122+
99123
"""
100124
path_filterer(top)
101125

test/packagebundler.jl

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,40 @@ end
267267
end
268268
end
269269

270+
@testset "cp_skip_dangling_symlinks" begin
271+
mktempdir() do src
272+
# Regular file
273+
write(joinpath(src, "regular.txt"), "hello")
274+
# Subdirectory with a nested file
275+
mkdir(joinpath(src, "subdir"))
276+
write(joinpath(src, "subdir", "nested.txt"), "world")
277+
# Valid symlink to an existing file
278+
symlink(joinpath(src, "regular.txt"), joinpath(src, "valid_link.txt"))
279+
# Dangling symlink (target does not exist)
280+
symlink(joinpath(src, "nonexistent.txt"), joinpath(src, "dangling_link.txt"))
281+
282+
dst = tempname()
283+
@test_logs (:warn, r"dangling") match_mode = :any JuliaHub._PackageBundler.cp_skip_dangling_symlinks(
284+
src, dst
285+
)
286+
287+
# Regular files and nested files are copied
288+
@test isfile(joinpath(dst, "regular.txt"))
289+
@test read(joinpath(dst, "regular.txt"), String) == "hello"
290+
@test isfile(joinpath(dst, "subdir", "nested.txt"))
291+
@test read(joinpath(dst, "subdir", "nested.txt"), String) == "world"
292+
293+
# Valid symlink is dereferenced: content present, no symlink at dst
294+
@test isfile(joinpath(dst, "valid_link.txt"))
295+
@test !islink(joinpath(dst, "valid_link.txt"))
296+
@test read(joinpath(dst, "valid_link.txt"), String) == "hello"
297+
298+
# Dangling symlink is silently skipped
299+
@test !ispath(joinpath(dst, "dangling_link.txt"))
300+
@test !islink(joinpath(dst, "dangling_link.txt"))
301+
end
302+
end
303+
270304
function bundle_and_file_listing(bundle_root_path::AbstractString)
271305
out = tempname()
272306
JuliaHub._PackageBundler.bundle(

0 commit comments

Comments
 (0)