Skip to content

Commit 4da0671

Browse files
KristofferCKristofferC
and
KristofferC
authored
prevent loading other extensions when precompiling an extension (#55589)
The current way of loading extensions when precompiling an extension very easily leads to cycles. For example, if you have more than one extension and you happen to transitively depend on the triggers of one of your extensions you will immediately hit a cycle where the extensions will try to load each other indefinitely. This is an issue because you cannot directly influence your transitive dependency graph so from this p.o.v the current system of loading extension is "unsound". The test added here checks this scenario and we can now precompile and load it without any warnings or issues. Would have made #55517 a non issue. Fixes #55557 --------- Co-authored-by: KristofferC <kristoffer.carlsson@juliacomputing.com>
1 parent 17445fe commit 4da0671

File tree

8 files changed

+76
-53
lines changed

8 files changed

+76
-53
lines changed

base/loading.jl

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,7 +1387,9 @@ function run_module_init(mod::Module, i::Int=1)
13871387
end
13881388

13891389
function run_package_callbacks(modkey::PkgId)
1390-
run_extension_callbacks(modkey)
1390+
if !precompiling_extension
1391+
run_extension_callbacks(modkey)
1392+
end
13911393
assert_havelock(require_lock)
13921394
unlock(require_lock)
13931395
try
@@ -2843,7 +2845,7 @@ end
28432845

28442846
const PRECOMPILE_TRACE_COMPILE = Ref{String}()
28452847
function create_expr_cache(pkg::PkgId, input::String, output::String, output_o::Union{Nothing, String},
2846-
concrete_deps::typeof(_concrete_dependencies), flags::Cmd=``, internal_stderr::IO = stderr, internal_stdout::IO = stdout)
2848+
concrete_deps::typeof(_concrete_dependencies), flags::Cmd=``, internal_stderr::IO = stderr, internal_stdout::IO = stdout, isext::Bool=false)
28472849
@nospecialize internal_stderr internal_stdout
28482850
rm(output, force=true) # Remove file if it exists
28492851
output_o === nothing || rm(output_o, force=true)
@@ -2912,7 +2914,7 @@ function create_expr_cache(pkg::PkgId, input::String, output::String, output_o::
29122914
write(io.in, """
29132915
empty!(Base.EXT_DORMITORY) # If we have a custom sysimage with `EXT_DORMITORY` prepopulated
29142916
Base.track_nested_precomp($precomp_stack)
2915-
Base.precompiling_extension = $(loading_extension)
2917+
Base.precompiling_extension = $(loading_extension | isext)
29162918
Base.precompiling_package = true
29172919
Base.include_package_for_output($(pkg_str(pkg)), $(repr(abspath(input))), $(repr(depot_path)), $(repr(dl_load_path)),
29182920
$(repr(load_path)), $deps, $(repr(source_path(nothing))))
@@ -2970,18 +2972,18 @@ This can be used to reduce package load times. Cache files are stored in
29702972
`DEPOT_PATH[1]/compiled`. See [Module initialization and precompilation](@ref)
29712973
for important notes.
29722974
"""
2973-
function compilecache(pkg::PkgId, internal_stderr::IO = stderr, internal_stdout::IO = stdout; flags::Cmd=``, reasons::Union{Dict{String,Int},Nothing}=Dict{String,Int}())
2975+
function compilecache(pkg::PkgId, internal_stderr::IO = stderr, internal_stdout::IO = stdout; flags::Cmd=``, reasons::Union{Dict{String,Int},Nothing}=Dict{String,Int}(), isext::Bool=false)
29742976
@nospecialize internal_stderr internal_stdout
29752977
path = locate_package(pkg)
29762978
path === nothing && throw(ArgumentError("$(repr("text/plain", pkg)) not found during precompilation"))
2977-
return compilecache(pkg, path, internal_stderr, internal_stdout; flags, reasons)
2979+
return compilecache(pkg, path, internal_stderr, internal_stdout; flags, reasons, isext)
29782980
end
29792981

29802982
const MAX_NUM_PRECOMPILE_FILES = Ref(10)
29812983

29822984
function compilecache(pkg::PkgId, path::String, internal_stderr::IO = stderr, internal_stdout::IO = stdout,
29832985
keep_loaded_modules::Bool = true; flags::Cmd=``, cacheflags::CacheFlags=CacheFlags(),
2984-
reasons::Union{Dict{String,Int},Nothing}=Dict{String,Int}())
2986+
reasons::Union{Dict{String,Int},Nothing}=Dict{String,Int}(), isext::Bool=false)
29852987

29862988
@nospecialize internal_stderr internal_stdout
29872989
# decide where to put the resulting cache file
@@ -3021,7 +3023,7 @@ function compilecache(pkg::PkgId, path::String, internal_stderr::IO = stderr, in
30213023
close(tmpio_o)
30223024
close(tmpio_so)
30233025
end
3024-
p = create_expr_cache(pkg, path, tmppath, tmppath_o, concrete_deps, flags, internal_stderr, internal_stdout)
3026+
p = create_expr_cache(pkg, path, tmppath, tmppath_o, concrete_deps, flags, internal_stderr, internal_stdout, isext)
30253027

30263028
if success(p)
30273029
if cache_objects

base/precompilation.jl

Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -435,51 +435,6 @@ function precompilepkgs(pkgs::Vector{String}=String[];
435435
# consider exts of direct deps to be direct deps so that errors are reported
436436
append!(direct_deps, keys(filter(d->last(d) in keys(env.project_deps), exts)))
437437

438-
# An extension effectively depends on another extension if it has all the the
439-
# dependencies of that other extension
440-
function expand_dependencies(depsmap)
441-
function visit!(visited, node, all_deps)
442-
if node in visited
443-
return
444-
end
445-
push!(visited, node)
446-
for dep in get(Set{Base.PkgId}, depsmap, node)
447-
if !(dep in all_deps)
448-
push!(all_deps, dep)
449-
visit!(visited, dep, all_deps)
450-
end
451-
end
452-
end
453-
454-
depsmap_transitive = Dict{Base.PkgId, Set{Base.PkgId}}()
455-
for package in keys(depsmap)
456-
# Initialize a set to keep track of all dependencies for 'package'
457-
all_deps = Set{Base.PkgId}()
458-
visited = Set{Base.PkgId}()
459-
visit!(visited, package, all_deps)
460-
# Update depsmap with the complete set of dependencies for 'package'
461-
depsmap_transitive[package] = all_deps
462-
end
463-
return depsmap_transitive
464-
end
465-
466-
depsmap_transitive = expand_dependencies(depsmap)
467-
468-
for (_, extensions_1) in pkg_exts_map
469-
for extension_1 in extensions_1
470-
deps_ext_1 = depsmap_transitive[extension_1]
471-
for (_, extensions_2) in pkg_exts_map
472-
for extension_2 in extensions_2
473-
extension_1 == extension_2 && continue
474-
deps_ext_2 = depsmap_transitive[extension_2]
475-
if issubset(deps_ext_2, deps_ext_1)
476-
push!(depsmap[extension_1], extension_2)
477-
end
478-
end
479-
end
480-
end
481-
end
482-
483438
@debug "precompile: deps collected"
484439
# this loop must be run after the full depsmap has been populated
485440
for (pkg, pkg_exts) in pkg_exts_map
@@ -852,7 +807,7 @@ function precompilepkgs(pkgs::Vector{String}=String[];
852807
t = @elapsed ret = precompile_pkgs_maybe_cachefile_lock(io, print_lock, fancyprint, pkg_config, pkgspidlocked, hascolor) do
853808
Base.with_logger(Base.NullLogger()) do
854809
# The false here means we ignore loaded modules, so precompile for a fresh session
855-
Base.compilecache(pkg, sourcepath, std_pipe, std_pipe, false; flags, cacheflags)
810+
Base.compilecache(pkg, sourcepath, std_pipe, std_pipe, false; flags, cacheflags, isext = haskey(exts, pkg))
856811
end
857812
end
858813
if ret isa Base.PrecompilableError

test/loading.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,6 +1155,19 @@ end
11551155
finally
11561156
copy!(LOAD_PATH, old_load_path)
11571157
end
1158+
1159+
# Extension with cycles in dependencies
1160+
code = """
1161+
using CyclicExtensions
1162+
Base.get_extension(CyclicExtensions, :ExtA) isa Module || error("expected extension to load")
1163+
Base.get_extension(CyclicExtensions, :ExtB) isa Module || error("expected extension to load")
1164+
CyclicExtensions.greet()
1165+
"""
1166+
proj = joinpath(@__DIR__, "project", "Extensions", "CyclicExtensions")
1167+
cmd = `$(Base.julia_cmd()) --startup-file=no -e $code`
1168+
cmd = addenv(cmd, "JULIA_LOAD_PATH" => proj)
1169+
@test occursin("Hello Cycles!", String(read(cmd)))
1170+
11581171
finally
11591172
try
11601173
rm(depot_path, force=true, recursive=true)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# This file is machine-generated - editing it directly is not advised
2+
3+
julia_version = "1.10.4"
4+
manifest_format = "2.0"
5+
project_hash = "ec25ff8df3a5e2212a173c3de2c7d716cc47cd36"
6+
7+
[[deps.ExtDep]]
8+
deps = ["SomePackage"]
9+
path = "../ExtDep.jl"
10+
uuid = "fa069be4-f60b-4d4c-8b95-f8008775090c"
11+
version = "0.1.0"
12+
13+
[[deps.ExtDep2]]
14+
path = "../ExtDep2"
15+
uuid = "55982ee5-2ad5-4c40-8cfe-5e9e1b01500d"
16+
version = "0.1.0"
17+
18+
[[deps.SomePackage]]
19+
path = "../SomePackage"
20+
uuid = "678608ae-7bb3-42c7-98b1-82102067a3d8"
21+
version = "0.1.0"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name = "CyclicExtensions"
2+
uuid = "17d4f0df-b55c-4714-ac4b-55fa23f7355c"
3+
version = "0.1.0"
4+
5+
[deps]
6+
ExtDep = "fa069be4-f60b-4d4c-8b95-f8008775090c"
7+
8+
[weakdeps]
9+
SomePackage = "678608ae-7bb3-42c7-98b1-82102067a3d8"
10+
11+
[extensions]
12+
ExtA = ["SomePackage"]
13+
ExtB = ["SomePackage"]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module ExtA
2+
3+
using CyclicExtensions
4+
using SomePackage
5+
6+
end
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module ExtB
2+
3+
using CyclicExtensions
4+
using SomePackage
5+
6+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module CyclicExtensions
2+
3+
using ExtDep
4+
5+
greet() = print("Hello Cycles!")
6+
7+
end # module CyclicExtensions

0 commit comments

Comments
 (0)