Skip to content

Commit 19f838f

Browse files
vtjnashKristofferC
authored andcommitted
finish implementation of upgradable stdlibs (#54739)
This now allows the user to load any number of copies of a module, and uses the combination of the environment, explicitly loaded modules, and the requirements of the precompile caches to determine the meaning of a name and which files need to be loaded. Note however that package extensions continue to primarily only apply to the explicitly loaded modules, although they may get loaded incidentally as the dependency of another package, they won't get defined for every pair of combinations of triggering modules. Fixes #53983 (cherry picked from commit a1a2ac6)
1 parent c42fc03 commit 19f838f

File tree

3 files changed

+96
-66
lines changed

3 files changed

+96
-66
lines changed

base/Base.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,10 @@ function __init__()
604604
init_active_project()
605605
append!(empty!(_sysimage_modules), keys(loaded_modules))
606606
empty!(explicit_loaded_modules)
607+
@assert isempty(loaded_precompiles)
608+
for (mod, key) in module_keys
609+
loaded_precompiles[key => module_build_id(mod)] = mod
610+
end
607611
if haskey(ENV, "JULIA_MAX_NUM_PRECOMPILE_FILES")
608612
MAX_NUM_PRECOMPILE_FILES[] = parse(Int, ENV["JULIA_MAX_NUM_PRECOMPILE_FILES"])
609613
end

base/loading.jl

Lines changed: 85 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,8 +1176,8 @@ function _include_from_serialized(pkg::PkgId, path::String, ocachepath::Union{No
11761176
dep = depmods[i]
11771177
dep isa Module && continue
11781178
_, depkey, depbuild_id = dep::Tuple{String, PkgId, UInt128}
1179-
@assert root_module_exists(depkey)
1180-
dep = root_module(depkey)
1179+
dep = loaded_precompiles[depkey => depbuild_id]
1180+
@assert PkgId(dep) == depkey && module_build_id(dep) === depbuild_id
11811181
depmods[i] = dep
11821182
end
11831183

@@ -1234,7 +1234,8 @@ function register_restored_modules(sv::SimpleVector, pkg::PkgId, path::String)
12341234
push!(Base.Docs.modules, M)
12351235
end
12361236
if parentmodule(M) === M
1237-
register_root_module(M)
1237+
push!(loaded_modules_order, M)
1238+
loaded_precompiles[pkg => module_build_id(M)] = M
12381239
end
12391240
end
12401241

@@ -1654,7 +1655,7 @@ function compilecache_path(pkg::PkgId;
16541655
if staledeps === true
16551656
continue
16561657
end
1657-
staledeps, _ = staledeps::Tuple{Vector{Any}, Union{Nothing, String}}
1658+
staledeps, _, _ = staledeps::Tuple{Vector{Any}, Union{Nothing, String}, UInt128}
16581659
# finish checking staledeps module graph
16591660
for i in 1:length(staledeps)
16601661
dep = staledeps[i]
@@ -1742,23 +1743,23 @@ end
17421743
# search for a precompile cache file to load, after some various checks
17431744
function _tryrequire_from_serialized(modkey::PkgId, build_id::UInt128)
17441745
assert_havelock(require_lock)
1745-
if root_module_exists(modkey)
1746-
loaded = root_module(modkey)
1747-
else
1746+
loaded = maybe_root_module(modkey)
1747+
if loaded === nothing
17481748
loaded = start_loading(modkey)
1749-
if loaded === nothing
1750-
try
1751-
modpath = locate_package(modkey)
1752-
modpath === nothing && return nothing
1753-
set_pkgorigin_version_path(modkey, String(modpath))
1754-
loaded = _require_search_from_serialized(modkey, String(modpath), build_id, true)
1755-
finally
1756-
end_loading(modkey, loaded)
1757-
end
1758-
if loaded isa Module
1759-
insert_extension_triggers(modkey)
1760-
run_package_callbacks(modkey)
1761-
end
1749+
end
1750+
if loaded === nothing
1751+
try
1752+
modpath = locate_package(modkey)
1753+
isnothing(modpath) && error("Cannot locate source for $(repr("text/plain", modkey))")
1754+
modpath = String(modpath)::String
1755+
set_pkgorigin_version_path(modkey, modpath)
1756+
loaded = _require_search_from_serialized(modkey, modpath, build_id, true)
1757+
finally
1758+
end_loading(modkey, loaded)
1759+
end
1760+
if loaded isa Module
1761+
insert_extension_triggers(modkey)
1762+
run_package_callbacks(modkey)
17621763
end
17631764
end
17641765
if loaded isa Module && PkgId(loaded) == modkey && module_build_id(loaded) === build_id
@@ -1831,10 +1832,12 @@ function _tryrequire_from_serialized(pkg::PkgId, path::String, ocachepath::Union
18311832
depmods[i] = dep
18321833
end
18331834
# then load the file
1834-
return _include_from_serialized(pkg, path, ocachepath, depmods, ignore_native)
1835+
loaded = _include_from_serialized(pkg, path, ocachepath, depmods, ignore_native)
1836+
loaded isa Module && register_root_module(loaded)
1837+
return loaded
18351838
end
18361839

1837-
# returns `nothing` if require found a precompile cache for this sourcepath, but couldn't load it
1840+
# returns `nothing` if require found a precompile cache for this sourcepath, but couldn't load it or it was stale
18381841
# returns the set of modules restored if the cache load succeeded
18391842
@constprop :none function _require_search_from_serialized(pkg::PkgId, sourcepath::String, build_id::UInt128, stalecheck::Bool; reasons=nothing, DEPOT_PATH::typeof(DEPOT_PATH)=DEPOT_PATH)
18401843
assert_havelock(require_lock)
@@ -1846,7 +1849,7 @@ end
18461849
continue
18471850
end
18481851
try
1849-
staledeps, ocachefile = staledeps::Tuple{Vector{Any}, Union{Nothing, String}}
1852+
staledeps, ocachefile, build_id = staledeps::Tuple{Vector{Any}, Union{Nothing, String}, UInt128}
18501853
# finish checking staledeps module graph
18511854
for i in 1:length(staledeps)
18521855
dep = staledeps[i]
@@ -1858,14 +1861,19 @@ end
18581861
if modstaledeps === true
18591862
continue
18601863
end
1861-
modstaledeps, modocachepath = modstaledeps::Tuple{Vector{Any}, Union{Nothing, String}}
1864+
modstaledeps, modocachepath, _ = modstaledeps::Tuple{Vector{Any}, Union{Nothing, String}, UInt128}
18621865
staledeps[i] = (modpath, modkey, modbuild_id, modpath_to_try, modstaledeps, modocachepath)
18631866
@goto check_next_dep
18641867
end
18651868
@debug "Rejecting cache file $path_to_try because required dependency $modkey with build ID $(UUID(modbuild_id)) is missing from the cache."
18661869
@goto check_next_path
18671870
@label check_next_dep
18681871
end
1872+
M = get(loaded_precompiles, pkg => build_id, nothing)
1873+
if isa(M, Module)
1874+
stalecheck && register_root_module(M)
1875+
return M
1876+
end
18691877
if stalecheck
18701878
try
18711879
touch(path_to_try) # update timestamp of precompilation file
@@ -1878,26 +1886,25 @@ end
18781886
dep = staledeps[i]
18791887
dep isa Module && continue
18801888
modpath, modkey, modbuild_id, modcachepath, modstaledeps, modocachepath = dep::Tuple{String, PkgId, UInt128, String, Vector{Any}, Union{Nothing, String}}
1881-
dep = nothing
1882-
if root_module_exists(modkey)
1883-
dep = root_module(modkey)
1889+
dep = get(loaded_precompiles, modkey => modbuild_id, nothing)
1890+
if dep === nothing
1891+
dep = maybe_root_module(modkey)
18841892
end
18851893
while true
18861894
if dep isa Module
18871895
if PkgId(dep) == modkey && module_build_id(dep) === modbuild_id
18881896
break
18891897
else
1890-
if stalecheck
1891-
@debug "Rejecting cache file $path_to_try because module $modkey is already loaded and incompatible."
1892-
@goto check_next_path
1893-
end
1898+
@debug "Rejecting cache file $path_to_try because module $modkey got loaded at a different version than expected."
1899+
@goto check_next_path
18941900
end
18951901
end
18961902
dep = start_loading(modkey)
18971903
if dep === nothing
18981904
try
18991905
set_pkgorigin_version_path(modkey, modpath)
19001906
dep = _include_from_serialized(modkey, modcachepath, modocachepath, modstaledeps)
1907+
dep isa Module && stalecheck && register_root_module(dep)
19011908
finally
19021909
end_loading(modkey, dep)
19031910
end
@@ -1911,7 +1918,11 @@ end
19111918
end
19121919
staledeps[i] = dep
19131920
end
1914-
restored = _include_from_serialized(pkg, path_to_try, ocachefile, staledeps)
1921+
restored = get(loaded_precompiles, pkg => build_id, nothing)
1922+
if !isa(restored, Module)
1923+
restored = _include_from_serialized(pkg, path_to_try, ocachefile, staledeps)
1924+
end
1925+
isa(restored, Module) && stalecheck && register_root_module(restored)
19151926
isa(restored, Module) && return restored
19161927
@debug "Deserialization checks failed while attempting to load cache from $path_to_try" exception=restored
19171928
@label check_next_path
@@ -2000,7 +2011,7 @@ const package_callbacks = Any[]
20002011
const include_callbacks = Any[]
20012012

20022013
# used to optionally track dependencies when requiring a module:
2003-
const _concrete_dependencies = Pair{PkgId,UInt128}[] # these dependency versions are "set in stone", and the process should try to avoid invalidating them
2014+
const _concrete_dependencies = Pair{PkgId,UInt128}[] # these dependency versions are "set in stone", because they are explicitly loaded, and the process should try to avoid invalidating them
20042015
const _require_dependencies = Any[] # a list of (mod, abspath, fsize, hash, mtime) tuples that are the file dependencies of the module currently being precompiled
20052016
const _track_dependencies = Ref(false) # set this to true to track the list of file dependencies
20062017
function _include_dependency(mod::Module, _path::AbstractString; track_content=true,
@@ -2251,15 +2262,20 @@ end
22512262
PkgOrigin() = PkgOrigin(nothing, nothing, nothing)
22522263
const pkgorigins = Dict{PkgId,PkgOrigin}()
22532264

2254-
const loaded_modules = Dict{PkgId,Module}()
2255-
# Emptied on Julia start
2256-
const explicit_loaded_modules = Dict{PkgId,Module}()
2265+
const explicit_loaded_modules = Dict{PkgId,Module}() # Emptied on Julia start
2266+
const loaded_modules = Dict{PkgId,Module}() # available to be explicitly loaded
2267+
const loaded_precompiles = Dict{Pair{PkgId,UInt128},Module}() # extended (complete) list of modules, available to be loaded
22572268
const loaded_modules_order = Vector{Module}()
2258-
const module_keys = IdDict{Module,PkgId}() # the reverse
2269+
const module_keys = IdDict{Module,PkgId}() # the reverse of loaded_modules
22592270

22602271
is_root_module(m::Module) = @lock require_lock haskey(module_keys, m)
22612272
root_module_key(m::Module) = @lock require_lock module_keys[m]
22622273

2274+
function module_build_id(m::Module)
2275+
hi, lo = ccall(:jl_module_build_id, NTuple{2,UInt64}, (Any,), m)
2276+
return (UInt128(hi) << 64) | lo
2277+
end
2278+
22632279
@constprop :none function register_root_module(m::Module)
22642280
# n.b. This is called from C after creating a new module in `Base.__toplevel__`,
22652281
# instead of adding them to the binding table there.
@@ -2275,7 +2291,7 @@ root_module_key(m::Module) = @lock require_lock module_keys[m]
22752291
end
22762292
end
22772293
end
2278-
push!(loaded_modules_order, m)
2294+
haskey(loaded_precompiles, key => module_build_id(m)) || push!(loaded_modules_order, m)
22792295
loaded_modules[key] = m
22802296
explicit_loaded_modules[key] = m
22812297
module_keys[m] = key
@@ -2307,6 +2323,9 @@ root_module_exists(key::PkgId) = @lock require_lock haskey(loaded_modules, key)
23072323
loaded_modules_array() = @lock require_lock copy(loaded_modules_order)
23082324

23092325
function unreference_module(key::PkgId)
2326+
if haskey(explicit_loaded_modules, key)
2327+
m = pop!(explicit_loaded_modules, key)
2328+
end
23102329
if haskey(loaded_modules, key)
23112330
m = pop!(loaded_modules, key)
23122331
# need to ensure all modules are GC rooted; will still be referenced
@@ -2450,7 +2469,7 @@ function _require(pkg::PkgId, env=nothing)
24502469
return loaded
24512470
end
24522471

2453-
# load a serialized file directly
2472+
# load a serialized file directly, including dependencies (without checking staleness except for immediate conflicts)
24542473
function _require_from_serialized(uuidkey::PkgId, path::String, ocachepath::Union{String, Nothing}, sourcepath::String)
24552474
@lock require_lock begin
24562475
set_pkgorigin_version_path(uuidkey, sourcepath)
@@ -2884,13 +2903,15 @@ function compilecache(pkg::PkgId, path::String, internal_stderr::IO = stderr, in
28842903
cachepath = compilecache_dir(pkg)
28852904

28862905
# build up the list of modules that we want the precompile process to preserve
2887-
concrete_deps = copy(_concrete_dependencies)
28882906
if keep_loaded_modules
2889-
for mod in loaded_modules_array()
2890-
if !(mod === Main || mod === Core || mod === Base)
2891-
push!(concrete_deps, PkgId(mod) => module_build_id(mod))
2907+
concrete_deps = copy(_concrete_dependencies)
2908+
for (pkgreq, modreq) in loaded_modules # TODO: convert all relevant staleness heuristics to use explicit_loaded_modules instead
2909+
if !(pkgreq === Main || pkgreq === Core || pkgreq === Base)
2910+
push!(concrete_deps, pkgreq => module_build_id(modreq))
28922911
end
28932912
end
2913+
else
2914+
concrete_deps = empty(_concrete_dependencies)
28942915
end
28952916
# run the expression and cache the result
28962917
verbosity = isinteractive() ? CoreLogging.Info : CoreLogging.Debug
@@ -3013,11 +3034,6 @@ function compilecache(pkg::PkgId, path::String, internal_stderr::IO = stderr, in
30133034
end
30143035
end
30153036

3016-
function module_build_id(m::Module)
3017-
hi, lo = ccall(:jl_module_build_id, NTuple{2,UInt64}, (Any,), m)
3018-
return (UInt128(hi) << 64) | lo
3019-
end
3020-
30213037
function isvalid_cache_header(f::IOStream)
30223038
pkgimage = Ref{UInt8}()
30233039
checksum = ccall(:jl_read_verify_header, UInt64, (Ptr{Cvoid}, Ptr{UInt8}, Ptr{Int64}, Ptr{Int64}), f.ios, pkgimage, Ref{Int64}(), Ref{Int64}()) # returns checksum id or zero
@@ -3570,7 +3586,7 @@ end
35703586
@constprop :none function stale_cachefile(modkey::PkgId, build_id::UInt128, modpath::String, cachefile::String;
35713587
ignore_loaded::Bool=false, requested_flags::CacheFlags=CacheFlags(),
35723588
reasons::Union{Dict{String,Int},Nothing}=nothing, stalecheck::Bool=true)
3573-
# XXX: this function appears to dl all of the file validation, not just those checks related to stale
3589+
# n.b.: this function does nearly all of the file validation, not just those checks related to stale, so the name is potentially unclear
35743590
io = open(cachefile, "r")
35753591
try
35763592
checksum = isvalid_cache_header(io)
@@ -3624,8 +3640,8 @@ end
36243640
record_reason(reasons, "for different pkgid")
36253641
return true
36263642
end
3643+
id_build = (UInt128(checksum) << 64) | id.second
36273644
if build_id != UInt128(0)
3628-
id_build = (UInt128(checksum) << 64) | id.second
36293645
if id_build != build_id
36303646
@debug "Ignoring cache file $cachefile for $modkey ($((UUID(id_build)))) since it does not provide desired build_id ($((UUID(build_id))))"
36313647
record_reason(reasons, "for different buildid")
@@ -3640,8 +3656,12 @@ end
36403656
depmods = Vector{Any}(undef, ndeps)
36413657
for i in 1:ndeps
36423658
req_key, req_build_id = required_modules[i]
3643-
# Module is already loaded
3644-
if root_module_exists(req_key)
3659+
# Check if module is already loaded
3660+
if !stalecheck && haskey(loaded_precompiles, req_key => req_build_id)
3661+
M = loaded_precompiles[req_key => req_build_id]
3662+
@assert PkgId(M) == req_key && module_build_id(M) === req_build_id
3663+
depmods[i] = M
3664+
elseif root_module_exists(req_key)
36453665
M = root_module(req_key)
36463666
if PkgId(M) == req_key && module_build_id(M) === req_build_id
36473667
depmods[i] = M
@@ -3672,17 +3692,19 @@ end
36723692
# check if this file is going to provide one of our concrete dependencies
36733693
# or if it provides a version that conflicts with our concrete dependencies
36743694
# or neither
3675-
for (req_key, req_build_id) in _concrete_dependencies
3676-
build_id = get(modules, req_key, UInt64(0))
3677-
if build_id !== UInt64(0)
3678-
build_id |= UInt128(checksum) << 64
3679-
if build_id === req_build_id
3680-
stalecheck = false
3681-
break
3695+
if stalecheck
3696+
for (req_key, req_build_id) in _concrete_dependencies
3697+
build_id = get(modules, req_key, UInt64(0))
3698+
if build_id !== UInt64(0)
3699+
build_id |= UInt128(checksum) << 64
3700+
if build_id === req_build_id
3701+
stalecheck = false
3702+
break
3703+
end
3704+
@debug "Rejecting cache file $cachefile because it provides the wrong build_id (got $((UUID(build_id)))) for $req_key (want $(UUID(req_build_id)))"
3705+
record_reason(reasons, "wrong dep buildid")
3706+
return true # cachefile doesn't provide the required version of the dependency
36823707
end
3683-
@debug "Rejecting cache file $cachefile because it provides the wrong build_id (got $((UUID(build_id)))) for $req_key (want $(UUID(req_build_id)))"
3684-
record_reason(reasons, "wrong dep buildid")
3685-
return true # cachefile doesn't provide the required version of the dependency
36863708
end
36873709
end
36883710

@@ -3770,7 +3792,7 @@ end
37703792
return true
37713793
end
37723794

3773-
return depmods, ocachefile # fresh cachefile
3795+
return depmods, ocachefile, id_build # fresh cachefile
37743796
finally
37753797
close(io)
37763798
end

test/precompile.jl

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -639,13 +639,13 @@ precompile_test_harness(false) do dir
639639
empty_prefs_hash = Base.get_preferences_hash(nothing, String[])
640640
@test cachefile == Base.compilecache_path(Base.PkgId("FooBar"), empty_prefs_hash)
641641
@test isfile(joinpath(cachedir, "FooBar.ji"))
642-
Tsc = Bool(Base.JLOptions().use_pkgimages) ? Tuple{<:Vector, String} : Tuple{<:Vector, Nothing}
642+
Tsc = Bool(Base.JLOptions().use_pkgimages) ? Tuple{<:Vector, String, UInt128} : Tuple{<:Vector, Nothing, UInt128}
643643
@test Base.stale_cachefile(FooBar_file, joinpath(cachedir, "FooBar.ji")) isa Tsc
644644
@test !isdefined(Main, :FooBar)
645645
@test !isdefined(Main, :FooBar1)
646646

647647
relFooBar_file = joinpath(dir, "subfolder", "..", "FooBar.jl")
648-
@test Base.stale_cachefile(relFooBar_file, joinpath(cachedir, "FooBar.ji")) isa (Sys.iswindows() ? Tuple{<:Vector, String} : Bool) # `..` is not a symlink on Windows
648+
@test Base.stale_cachefile(relFooBar_file, joinpath(cachedir, "FooBar.ji")) isa (Sys.iswindows() ? Tuple{<:Vector, String, UInt128} : Bool) # `..` is not a symlink on Windows
649649
mkdir(joinpath(dir, "subfolder"))
650650
@test Base.stale_cachefile(relFooBar_file, joinpath(cachedir, "FooBar.ji")) isa Tsc
651651

@@ -1546,6 +1546,7 @@ precompile_test_harness("Issue #26028") do load_path
15461546
module Foo26028
15471547
module Bar26028
15481548
x = 0
1549+
y = 0
15491550
end
15501551
function __init__()
15511552
include(joinpath(@__DIR__, "Baz26028.jl"))
@@ -1555,7 +1556,10 @@ precompile_test_harness("Issue #26028") do load_path
15551556
write(joinpath(load_path, "Baz26028.jl"),
15561557
"""
15571558
module Baz26028
1558-
import Foo26028.Bar26028.x
1559+
using Test
1560+
@test_throws(ConcurrencyViolationError("deadlock detected in loading Foo26028 -> Foo26028"),
1561+
@eval import Foo26028.Bar26028.x)
1562+
import ..Foo26028.Bar26028.y
15591563
end
15601564
""")
15611565
Base.compilecache(Base.PkgId("Foo26028"))

0 commit comments

Comments
 (0)