diff --git a/base/loading.jl b/base/loading.jl index db6a681bb2a5b..d83c899eda807 100644 --- a/base/loading.jl +++ b/base/loading.jl @@ -3298,14 +3298,22 @@ mutable struct CacheHeaderIncludes const modpath::Vector{String} # seemingly not needed in Base, but used by Revise end -function replace_depot_path(path::AbstractString, depots::Vector{String}=normalize_depots_for_relocation()) +function replace_depot_path_impl(path::AbstractString, depots::Vector{String}=normalize_depots_for_relocation()) + the_depot = nothing + reloc_path = path for depot in depots if startswith(path, string(depot, Filesystem.pathsep())) || path == depot - path = replace(path, depot => "@depot"; count=1) + reloc_path = replace(path, depot => "@depot"; count=1) + the_depot = depot break end end - return path + return the_depot, reloc_path +end + +function replace_depot_path(path::AbstractString, depots::Vector{String}=normalize_depots_for_relocation()) + _, reloc_path = replace_depot_path_impl(path, depots) + return reloc_path end function normalize_depots_for_relocation() @@ -3333,6 +3341,65 @@ function resolve_depot(inc::AbstractString) return :no_depot_found end +""" + Base.RelocPath(path::AbstractString) + +A type to represent a relocatable path. + +Requires `path` to be located within one of `DEPOT_PATH` upon construction. + +An error is thrown if relocation fails. + +# Example +```jldoctest +julia> path1 = joinpath(mktempdir(), "foo"); touch(path1); # set up a file called foo + +julia> pushfirst!(DEPOT_PATH, dirname(path1)); + +julia> relocpath = Base.RelocPath(path1); + +julia> String(relocpath) == path1 +true + +julia> path2 = joinpath(mktempdir(), "foo"); touch(path2); # set up another foo + +julia> pushfirst!(DEPOT_PATH, dirname(path2)); + +julia> String(relocpath) == path2 +true + +julia> path1 != path2 +true +``` +""" +struct RelocPath + subpath::String + function RelocPath(path::AbstractString) + depot, _ = replace_depot_path_impl(path) + if isnothing(depot) + error("Failed to locate $(path) in any of DEPOT_PATH.") + end + subpath = replace(path, depot => ""; count=1) + return new(subpath) + end +end + +function String(r::RelocPath) + for d in DEPOT_PATH + if isdirpath(d) + d = dirname(d) + end + path = string(d, r.subpath) + if ispath(path) + return path + end + end + error("Failed to relocate @depot$(r.subpath) in any of DEPOT_PATH.") +end + +function show(io::IO, r::RelocPath) + print(io, string("RelocPath(\"@depot", r.subpath, "\")")) +end function _parse_cache_header(f::IO, cachefile::AbstractString) flags = read(f, UInt8) diff --git a/base/public.jl b/base/public.jl index 1a23550485d84..e90c73353b394 100644 --- a/base/public.jl +++ b/base/public.jl @@ -45,6 +45,7 @@ public DL_LOAD_PATH, load_path, active_project, + RelocPath, # Reflection and introspection isambiguous, diff --git a/test/RelocationTestPkg1/src/RelocationTestPkg1.jl b/test/RelocationTestPkg1/src/RelocationTestPkg1.jl index a86543a61b3f8..4556b21b6d7a2 100644 --- a/test/RelocationTestPkg1/src/RelocationTestPkg1.jl +++ b/test/RelocationTestPkg1/src/RelocationTestPkg1.jl @@ -1,5 +1,7 @@ module RelocationTestPkg1 +const relocpath = Base.RelocPath(@__DIR__) + greet() = print("Hello World!") end # module RelocationTestPkg1 diff --git a/test/relocatedepot.jl b/test/relocatedepot.jl index 2ef6dec90dbc1..6bc02bad51d18 100644 --- a/test/relocatedepot.jl +++ b/test/relocatedepot.jl @@ -20,7 +20,7 @@ function test_harness(@nospecialize(fn); empty_load_path=true, empty_depot_path= end # We test relocation with these dummy pkgs: -# - RelocationTestPkg1 - pkg with no include_dependency +# - RelocationTestPkg1 - pkg with no include_dependency, also contains a RelocPath() # - RelocationTestPkg2 - pkg with include_dependency tracked by `mtime` # - RelocationTestPkg3 - pkg with include_dependency tracked by content # - RelocationTestPkg4 - pkg with no dependencies; will be compiled such that the pkgimage is @@ -263,11 +263,18 @@ else pkgname = "RelocationTestPkg1" test_harness() do push!(LOAD_PATH, joinpath(@__DIR__, "relocatedepot")) - push!(DEPOT_PATH, joinpath(@__DIR__, "relocatedepot")) # required to find src files push!(DEPOT_PATH, joinpath(@__DIR__, "relocatedepot", "julia")) # contains cache file + push!(DEPOT_PATH, joinpath(@__DIR__, "relocatedepot")) # required to find src files pkg = Base.identify_package(pkgname) @test Base.isprecompiled(pkg) == true @test Base.isrelocatable(pkg) == true + pkg = Base.require(pkg) + @test String(pkg.relocpath) == joinpath(pkgdir(pkg), "src") + pop!(DEPOT_PATH) + @test_throws( + ErrorException("Failed to relocate @depot/RelocationTestPkg1/src in any of DEPOT_PATH."), + String(pkg.relocpath) + ) end end