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/src/internal.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
```@docs
JuliaHub._PackageBundler.bundle
JuliaHub._PackageBundler.path_filterer
JuliaHub._PackageBundler.cp_skip_dangling_symlinks
```

## Index
Expand Down
2 changes: 1 addition & 1 deletion src/PackageBundler/PackageBundler.jl
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function bundle(dir; output="", force=false, allownoenv=false, verbose=true)::St
# add the depot and such, and finally tar all that up.
tmp_dir = mktempdir()
output_dir = joinpath(tmp_dir, name)
cp(dir, output_dir; follow_symlinks=true)
cp_skip_dangling_symlinks(dir, output_dir)
bundle_dir = joinpath(output_dir, ".bundle")
mkpath(bundle_dir)
# Bundle artifacts
Expand Down
24 changes: 24 additions & 0 deletions src/PackageBundler/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,30 @@ function get_bundleignore(file, top)
return patterns, top
end

"""
cp_skip_dangling_symlinks(src, dst)

Recursively copies the directory `src` to `dst`, mirroring the behaviour of
`cp(src, dst; follow_symlinks=true)` but silently skipping any dangling symlinks
(symlinks whose target does not exist) instead of erroring on them.
"""
function cp_skip_dangling_symlinks(src::AbstractString, dst::AbstractString)
mkpath(dst)
for entry in readdir(src)
src_entry = joinpath(src, entry)
dst_entry = joinpath(dst, entry)
if isdir(src_entry)
cp_skip_dangling_symlinks(src_entry, dst_entry)
elseif isfile(src_entry)
cp(src_entry, dst_entry; follow_symlinks=true)
elseif islink(src_entry)
# Dangling symlink: isfile/isdir follow symlinks so both return false for a
# dangling symlink, while islink uses lstat so it returns true.
@warn "Skipping dangling symlink" path = src_entry
end
end
end

"""
path_filterer(top)

Expand Down
34 changes: 34 additions & 0 deletions test/packagebundler.jl
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,40 @@ end
end
end

@testset "cp_skip_dangling_symlinks" begin
mktempdir() do src
# Regular file
write(joinpath(src, "regular.txt"), "hello")
# Subdirectory with a nested file
mkdir(joinpath(src, "subdir"))
write(joinpath(src, "subdir", "nested.txt"), "world")
# Valid symlink to an existing file
symlink(joinpath(src, "regular.txt"), joinpath(src, "valid_link.txt"))
# Dangling symlink (target does not exist)
symlink(joinpath(src, "nonexistent.txt"), joinpath(src, "dangling_link.txt"))

dst = tempname()
@test_logs (:warn, r"dangling") match_mode = :any JuliaHub._PackageBundler.cp_skip_dangling_symlinks(
src, dst
)

# Regular files and nested files are copied
@test isfile(joinpath(dst, "regular.txt"))
@test read(joinpath(dst, "regular.txt"), String) == "hello"
@test isfile(joinpath(dst, "subdir", "nested.txt"))
@test read(joinpath(dst, "subdir", "nested.txt"), String) == "world"

# Valid symlink is dereferenced: content present, no symlink at dst
@test isfile(joinpath(dst, "valid_link.txt"))
@test !islink(joinpath(dst, "valid_link.txt"))
@test read(joinpath(dst, "valid_link.txt"), String) == "hello"

# Dangling symlink is silently skipped
@test !ispath(joinpath(dst, "dangling_link.txt"))
@test !islink(joinpath(dst, "dangling_link.txt"))
end
end

function bundle_and_file_listing(bundle_root_path::AbstractString)
out = tempname()
JuliaHub._PackageBundler.bundle(
Expand Down
Loading