diff --git a/docs/src/internal.md b/docs/src/internal.md index 8a061cd133..3129684096 100644 --- a/docs/src/internal.md +++ b/docs/src/internal.md @@ -5,6 +5,7 @@ ```@docs JuliaHub._PackageBundler.bundle JuliaHub._PackageBundler.path_filterer +JuliaHub._PackageBundler.cp_skip_dangling_symlinks ``` ## Index diff --git a/src/PackageBundler/PackageBundler.jl b/src/PackageBundler/PackageBundler.jl index 435abe192a..60e1f2dc06 100644 --- a/src/PackageBundler/PackageBundler.jl +++ b/src/PackageBundler/PackageBundler.jl @@ -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 diff --git a/src/PackageBundler/utils.jl b/src/PackageBundler/utils.jl index 81163d575f..914ab4b9ed 100644 --- a/src/PackageBundler/utils.jl +++ b/src/PackageBundler/utils.jl @@ -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) diff --git a/test/packagebundler.jl b/test/packagebundler.jl index d9cf7ab7b3..68d4bccc1e 100644 --- a/test/packagebundler.jl +++ b/test/packagebundler.jl @@ -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(