Skip to content

Commit bf5675b

Browse files
authored
[backports-release-1.11] Allow ext → ext dependency if triggers are a strict superset (#56368)
This is an effort at a proper workaround (feature?) to resolve #56204. For example: ```toml [extensions] PlottingExt = "Plots" StatisticsPlottingExt = ["Plots", "Statistics"] ``` Here `StatisticsPlottingExt` is allowed to depend on `PlottingExt` This provides a way to declare `ext → ext` dependencies while still avoiding any extension cycles. The same trick can also be used to make an extension in one package depend on an extension provided in another. We'll also need to land #49891, so that we guarantee these load in the right order.
1 parent 23cb128 commit bf5675b

File tree

22 files changed

+297
-50
lines changed

22 files changed

+297
-50
lines changed

NEWS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ Language changes
3737
omit the default user depot ([#51448]).
3838
* Precompilation cache files are now relocatable and their validity is now verified through
3939
a content hash of their source files instead of their `mtime` ([#49866]).
40+
* Extensions may now depend on other extensions, if their triggers include all triggers of any
41+
extension they wish to depend upon (+ at least one other trigger). Ext-to-ext dependencies
42+
that don't meet this requirement are now blocked from using `Base.get_extension` during pre-
43+
compilation, to prevent extension cycles [#55557].
4044

4145
Compiler/Runtime improvements
4246
-----------------------------

base/loading.jl

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -733,8 +733,9 @@ function manifest_uuid_path(env::String, pkg::PkgId)::Union{Nothing,String,Missi
733733
proj = implicit_manifest_uuid_path(env, pkg)
734734
proj === nothing || return proj
735735
# if not found
736-
parentid = get(EXT_PRIMED, pkg, nothing)
737-
if parentid !== nothing
736+
triggers = get(EXT_PRIMED, pkg, nothing)
737+
if triggers !== nothing
738+
parentid = triggers[1]
738739
_, parent_project_file = entry_point_and_project_file(env, parentid.name)
739740
if parent_project_file !== nothing
740741
parentproj = project_file_name_uuid(parent_project_file, parentid.name)
@@ -1387,9 +1388,7 @@ function run_module_init(mod::Module, i::Int=1)
13871388
end
13881389

13891390
function run_package_callbacks(modkey::PkgId)
1390-
if !precompiling_extension
1391-
run_extension_callbacks(modkey)
1392-
end
1391+
run_extension_callbacks(modkey)
13931392
assert_havelock(require_lock)
13941393
unlock(require_lock)
13951394
try
@@ -1418,7 +1417,7 @@ mutable struct ExtensionId
14181417
ntriggers::Int # how many more packages must be defined until this is loaded
14191418
end
14201419

1421-
const EXT_PRIMED = Dict{PkgId, PkgId}() # Extension -> Parent
1420+
const EXT_PRIMED = Dict{PkgId,Vector{PkgId}}() # Extension -> Parent + Triggers (parent is always first)
14221421
const EXT_DORMITORY = Dict{PkgId,Vector{ExtensionId}}() # Trigger -> Extensions that can be triggered by it
14231422
const EXT_DORMITORY_FAILED = ExtensionId[]
14241423

@@ -1509,14 +1508,15 @@ function _insert_extension_triggers(parent::PkgId, extensions::Dict{String, Any}
15091508
if haskey(EXT_PRIMED, id) || haskey(Base.loaded_modules, id)
15101509
continue # extension is already primed or loaded, don't add it again
15111510
end
1512-
EXT_PRIMED[id] = parent
1511+
EXT_PRIMED[id] = trigger_ids = PkgId[parent]
15131512
gid = ExtensionId(id, parent, 1 + length(triggers), 1 + length(triggers))
15141513
trigger1 = get!(Vector{ExtensionId}, EXT_DORMITORY, parent)
15151514
push!(trigger1, gid)
15161515
for trigger in triggers
15171516
# TODO: Better error message if this lookup fails?
15181517
uuid_trigger = UUID(totaldeps[trigger]::String)
15191518
trigger_id = PkgId(uuid_trigger, trigger)
1519+
push!(trigger_ids, trigger_id)
15201520
if !haskey(explicit_loaded_modules, trigger_id) || haskey(package_locks, trigger_id)
15211521
trigger1 = get!(Vector{ExtensionId}, EXT_DORMITORY, trigger_id)
15221522
push!(trigger1, gid)
@@ -1528,6 +1528,7 @@ function _insert_extension_triggers(parent::PkgId, extensions::Dict{String, Any}
15281528
end
15291529

15301530
loading_extension::Bool = false
1531+
loadable_extensions::Union{Nothing,Vector{PkgId}} = nothing
15311532
precompiling_extension::Bool = false
15321533
function run_extension_callbacks(extid::ExtensionId)
15331534
assert_havelock(require_lock)
@@ -1558,7 +1559,7 @@ function run_extension_callbacks(pkgid::PkgId)
15581559
for extid in extids
15591560
@assert extid.ntriggers > 0
15601561
extid.ntriggers -= 1
1561-
if extid.ntriggers == 0
1562+
if extid.ntriggers == 0 && (loadable_extensions === nothing || extid.id in loadable_extensions)
15621563
push!(extids_to_load, extid)
15631564
end
15641565
end
@@ -2542,7 +2543,17 @@ function _require(pkg::PkgId, env=nothing)
25422543
# double-check the search now that we have lock
25432544
m = _require_search_from_serialized(pkg, path, UInt128(0), true)
25442545
m isa Module && return m
2545-
return compilecache(pkg, path; reasons)
2546+
triggers = get(EXT_PRIMED, pkg, nothing)
2547+
loadable_exts = nothing
2548+
if triggers !== nothing # extension
2549+
loadable_exts = PkgId[]
2550+
for (ext′, triggers′) in EXT_PRIMED
2551+
if triggers′ triggers
2552+
push!(loadable_exts, ext′)
2553+
end
2554+
end
2555+
end
2556+
return compilecache(pkg, path; reasons, loadable_exts)
25462557
end
25472558
loaded isa Module && return loaded
25482559
if isnothing(loaded) # maybe_cachefile_lock returns nothing if it had to wait for another process
@@ -2865,19 +2876,26 @@ function check_package_module_loaded(pkg::PkgId)
28652876
return nothing
28662877
end
28672878

2879+
# protects against PkgId and UUID being imported and losing Base prefix
2880+
_pkg_str(_pkg::PkgId) = (_pkg.uuid === nothing) ? "Base.PkgId($(repr(_pkg.name)))" : "Base.PkgId(Base.UUID(\"$(_pkg.uuid)\"), $(repr(_pkg.name)))"
2881+
_pkg_str(_pkg::Vector) = sprint(show, eltype(_pkg); context = :module=>nothing) * "[" * join(map(_pkg_str, _pkg), ",") * "]"
2882+
_pkg_str(_pkg::Pair{PkgId}) = _pkg_str(_pkg.first) * " => " * repr(_pkg.second)
2883+
_pkg_str(_pkg::Nothing) = "nothing"
2884+
28682885
const PRECOMPILE_TRACE_COMPILE = Ref{String}()
28692886
function create_expr_cache(pkg::PkgId, input::String, output::String, output_o::Union{Nothing, String},
28702887
concrete_deps::typeof(_concrete_dependencies), flags::Cmd=``, cacheflags::CacheFlags=CacheFlags(),
2871-
internal_stderr::IO = stderr, internal_stdout::IO = stdout, isext::Bool=false)
2888+
internal_stderr::IO = stderr, internal_stdout::IO = stdout, loadable_exts::Union{Vector{PkgId},Nothing}=nothing)
28722889
@nospecialize internal_stderr internal_stdout
28732890
rm(output, force=true) # Remove file if it exists
28742891
output_o === nothing || rm(output_o, force=true)
28752892
depot_path = map(abspath, DEPOT_PATH)
28762893
dl_load_path = map(abspath, DL_LOAD_PATH)
28772894
load_path = map(abspath, Base.load_path())
28782895
# if pkg is a stdlib, append its parent Project.toml to the load path
2879-
parentid = get(EXT_PRIMED, pkg, nothing)
2880-
if parentid !== nothing
2896+
triggers = get(EXT_PRIMED, pkg, nothing)
2897+
if triggers !== nothing
2898+
parentid = triggers[1]
28812899
for env in load_path
28822900
project_file = env_project_file(env)
28832901
if project_file === true
@@ -2895,22 +2913,6 @@ function create_expr_cache(pkg::PkgId, input::String, output::String, output_o::
28952913
any(path -> path_sep in path, load_path) &&
28962914
error("LOAD_PATH entries cannot contain $(repr(path_sep))")
28972915

2898-
deps_strs = String[]
2899-
# protects against PkgId and UUID being imported and losing Base prefix
2900-
function pkg_str(_pkg::PkgId)
2901-
if _pkg.uuid === nothing
2902-
"Base.PkgId($(repr(_pkg.name)))"
2903-
else
2904-
"Base.PkgId(Base.UUID(\"$(_pkg.uuid)\"), $(repr(_pkg.name)))"
2905-
end
2906-
end
2907-
for (pkg, build_id) in concrete_deps
2908-
push!(deps_strs, "$(pkg_str(pkg)) => $(repr(build_id))")
2909-
end
2910-
deps_eltype = sprint(show, eltype(concrete_deps); context = :module=>nothing)
2911-
deps = deps_eltype * "[" * join(deps_strs, ",") * "]"
2912-
precomp_stack = "Base.PkgId[$(join(map(pkg_str, vcat(Base.precompilation_stack, pkg)), ", "))]"
2913-
29142916
if output_o === nothing
29152917
# remove options that make no difference given the other cache options
29162918
cacheflags = CacheFlags(cacheflags, opt_level=0)
@@ -2941,10 +2943,11 @@ function create_expr_cache(pkg::PkgId, input::String, output::String, output_o::
29412943
# write data over stdin to avoid the (unlikely) case of exceeding max command line size
29422944
write(io.in, """
29432945
empty!(Base.EXT_DORMITORY) # If we have a custom sysimage with `EXT_DORMITORY` prepopulated
2944-
Base.track_nested_precomp($precomp_stack)
2945-
Base.precompiling_extension = $(loading_extension | isext)
2946-
Base.include_package_for_output($(pkg_str(pkg)), $(repr(abspath(input))), $(repr(depot_path)), $(repr(dl_load_path)),
2947-
$(repr(load_path)), $deps, $(repr(source_path(nothing))))
2946+
Base.track_nested_precomp($(_pkg_str(vcat(Base.precompilation_stack, pkg))))
2947+
Base.loadable_extensions = $(_pkg_str(loadable_exts))
2948+
Base.precompiling_extension = $(loading_extension)
2949+
Base.include_package_for_output($(_pkg_str(pkg)), $(repr(abspath(input))), $(repr(depot_path)), $(repr(dl_load_path)),
2950+
$(repr(load_path)), $(_pkg_str(concrete_deps)), $(repr(source_path(nothing))))
29482951
""")
29492952
close(io.in)
29502953
return io
@@ -2999,18 +3002,18 @@ This can be used to reduce package load times. Cache files are stored in
29993002
`DEPOT_PATH[1]/compiled`. See [Module initialization and precompilation](@ref)
30003003
for important notes.
30013004
"""
3002-
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)
3005+
function compilecache(pkg::PkgId, internal_stderr::IO = stderr, internal_stdout::IO = stdout; flags::Cmd=``, reasons::Union{Dict{String,Int},Nothing}=Dict{String,Int}(), loadable_exts::Union{Vector{PkgId},Nothing}=nothing)
30033006
@nospecialize internal_stderr internal_stdout
30043007
path = locate_package(pkg)
30053008
path === nothing && throw(ArgumentError("$(repr("text/plain", pkg)) not found during precompilation"))
3006-
return compilecache(pkg, path, internal_stderr, internal_stdout; flags, reasons, isext)
3009+
return compilecache(pkg, path, internal_stderr, internal_stdout; flags, reasons, loadable_exts)
30073010
end
30083011

30093012
const MAX_NUM_PRECOMPILE_FILES = Ref(10)
30103013

30113014
function compilecache(pkg::PkgId, path::String, internal_stderr::IO = stderr, internal_stdout::IO = stdout,
30123015
keep_loaded_modules::Bool = true; flags::Cmd=``, cacheflags::CacheFlags=CacheFlags(),
3013-
reasons::Union{Dict{String,Int},Nothing}=Dict{String,Int}(), isext::Bool=false)
3016+
reasons::Union{Dict{String,Int},Nothing}=Dict{String,Int}(), loadable_exts::Union{Vector{PkgId},Nothing}=nothing)
30143017

30153018
@nospecialize internal_stderr internal_stdout
30163019
# decide where to put the resulting cache file
@@ -3050,7 +3053,7 @@ function compilecache(pkg::PkgId, path::String, internal_stderr::IO = stderr, in
30503053
close(tmpio_o)
30513054
close(tmpio_so)
30523055
end
3053-
p = create_expr_cache(pkg, path, tmppath, tmppath_o, concrete_deps, flags, cacheflags, internal_stderr, internal_stdout, isext)
3056+
p = create_expr_cache(pkg, path, tmppath, tmppath_o, concrete_deps, flags, cacheflags, internal_stderr, internal_stdout, loadable_exts)
30543057

30553058
if success(p)
30563059
if cache_objects

base/precompilation.jl

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ function _precompilepkgs(pkgs::Vector{String},
419419
return name
420420
end
421421

422+
triggers = Dict{Base.PkgId,Vector{Base.PkgId}}()
422423
for (dep, deps) in env.deps
423424
pkg = Base.PkgId(dep, env.names[dep])
424425
Base.in_sysimage(pkg) && continue
@@ -427,25 +428,22 @@ function _precompilepkgs(pkgs::Vector{String},
427428
# add any extensions
428429
pkg_exts = Dict{Base.PkgId, Vector{Base.PkgId}}()
429430
for (ext_name, extdep_uuids) in env.extensions[dep]
430-
ext_deps = Base.PkgId[]
431-
push!(ext_deps, pkg) # depends on parent package
431+
ext_uuid = Base.uuid5(pkg.uuid, ext_name)
432+
ext = Base.PkgId(ext_uuid, ext_name)
433+
triggers[ext] = Base.PkgId[pkg] # depends on parent package
432434
all_extdeps_available = true
433435
for extdep_uuid in extdep_uuids
434436
extdep_name = env.names[extdep_uuid]
435437
if extdep_uuid in keys(env.deps)
436-
push!(ext_deps, Base.PkgId(extdep_uuid, extdep_name))
438+
push!(triggers[ext], Base.PkgId(extdep_uuid, extdep_name))
437439
else
438440
all_extdeps_available = false
439441
break
440442
end
441443
end
442444
all_extdeps_available || continue
443-
ext_uuid = Base.uuid5(pkg.uuid, ext_name)
444-
ext = Base.PkgId(ext_uuid, ext_name)
445-
filter!(!Base.in_sysimage, ext_deps)
446-
depsmap[ext] = ext_deps
447445
exts[ext] = pkg.name
448-
pkg_exts[ext] = ext_deps
446+
pkg_exts[ext] = depsmap[ext] = filter(!Base.in_sysimage, triggers[ext])
449447
end
450448
if !isempty(pkg_exts)
451449
pkg_exts_map[pkg] = collect(keys(pkg_exts))
@@ -461,6 +459,16 @@ function _precompilepkgs(pkgs::Vector{String},
461459
append!(direct_deps, keys(filter(d->last(d) in keys(env.project_deps), exts)))
462460

463461
@debug "precompile: deps collected"
462+
463+
# An extension effectively depends on another extension if it has a strict superset of its triggers
464+
for ext_a in keys(exts)
465+
for ext_b in keys(exts)
466+
if triggers[ext_a] triggers[ext_b]
467+
push!(depsmap[ext_a], ext_b)
468+
end
469+
end
470+
end
471+
464472
# this loop must be run after the full depsmap has been populated
465473
for (pkg, pkg_exts) in pkg_exts_map
466474
# find any packages that depend on the extension(s)'s deps and replace those deps in their deps list with the extension(s),
@@ -817,7 +825,12 @@ function _precompilepkgs(pkgs::Vector{String},
817825
t = @elapsed ret = precompile_pkgs_maybe_cachefile_lock(io, print_lock, fancyprint, pkg_config, pkgspidlocked, hascolor) do
818826
Base.with_logger(Base.NullLogger()) do
819827
# The false here means we ignore loaded modules, so precompile for a fresh session
820-
Base.compilecache(pkg, sourcepath, std_pipe, std_pipe, false; flags, cacheflags, isext = haskey(exts, pkg))
828+
keep_loaded_modules = false
829+
# for extensions, any extension in our direct dependencies is one we have a right to load
830+
# for packages, we may load any extension (all possible triggers are accounted for above)
831+
loadable_exts = haskey(exts, pkg) ? filter((dep)->haskey(exts, dep), depsmap[pkg]) : nothing
832+
Base.compilecache(pkg, sourcepath, std_pipe, std_pipe, keep_loaded_modules;
833+
flags, cacheflags, loadable_exts)
821834
end
822835
end
823836
if ret isa Base.PrecompilableError

test/loading.jl

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,74 @@ end
11581158
cmd = addenv(cmd, "JULIA_LOAD_PATH" => proj)
11591159
@test occursin("Hello Cycles!", String(read(cmd)))
11601160

1161+
# Extension-to-extension dependencies
1162+
1163+
mktempdir() do depot # Parallel pre-compilation
1164+
code = """
1165+
Base.disable_parallel_precompile = false
1166+
using ExtToExtDependency
1167+
Base.get_extension(ExtToExtDependency, :ExtA) isa Module || error("expected extension to load")
1168+
Base.get_extension(ExtToExtDependency, :ExtAB) isa Module || error("expected extension to load")
1169+
ExtToExtDependency.greet()
1170+
"""
1171+
proj = joinpath(@__DIR__, "project", "Extensions", "ExtToExtDependency")
1172+
cmd = `$(Base.julia_cmd()) --startup-file=no -e $code`
1173+
cmd = addenv(cmd,
1174+
"JULIA_LOAD_PATH" => proj,
1175+
"JULIA_DEPOT_PATH" => depot * Base.Filesystem.pathsep(),
1176+
)
1177+
@test occursin("Hello ext-to-ext!", String(read(cmd)))
1178+
end
1179+
mktempdir() do depot # Serial pre-compilation
1180+
code = """
1181+
Base.disable_parallel_precompile = true
1182+
using ExtToExtDependency
1183+
Base.get_extension(ExtToExtDependency, :ExtA) isa Module || error("expected extension to load")
1184+
Base.get_extension(ExtToExtDependency, :ExtAB) isa Module || error("expected extension to load")
1185+
ExtToExtDependency.greet()
1186+
"""
1187+
proj = joinpath(@__DIR__, "project", "Extensions", "ExtToExtDependency")
1188+
cmd = `$(Base.julia_cmd()) --startup-file=no -e $code`
1189+
cmd = addenv(cmd,
1190+
"JULIA_LOAD_PATH" => proj,
1191+
"JULIA_DEPOT_PATH" => depot * Base.Filesystem.pathsep(),
1192+
)
1193+
@test occursin("Hello ext-to-ext!", String(read(cmd)))
1194+
end
1195+
1196+
mktempdir() do depot # Parallel pre-compilation
1197+
code = """
1198+
Base.disable_parallel_precompile = false
1199+
using CrossPackageExtToExtDependency
1200+
Base.get_extension(CrossPackageExtToExtDependency.CyclicExtensions, :ExtA) isa Module || error("expected extension to load")
1201+
Base.get_extension(CrossPackageExtToExtDependency, :ExtAB) isa Module || error("expected extension to load")
1202+
CrossPackageExtToExtDependency.greet()
1203+
"""
1204+
proj = joinpath(@__DIR__, "project", "Extensions", "CrossPackageExtToExtDependency")
1205+
cmd = `$(Base.julia_cmd()) --startup-file=no -e $code`
1206+
cmd = addenv(cmd,
1207+
"JULIA_LOAD_PATH" => proj,
1208+
"JULIA_DEPOT_PATH" => depot * Base.Filesystem.pathsep(),
1209+
)
1210+
@test occursin("Hello x-package ext-to-ext!", String(read(cmd)))
1211+
end
1212+
mktempdir() do depot # Serial pre-compilation
1213+
code = """
1214+
Base.disable_parallel_precompile = true
1215+
using CrossPackageExtToExtDependency
1216+
Base.get_extension(CrossPackageExtToExtDependency.CyclicExtensions, :ExtA) isa Module || error("expected extension to load")
1217+
Base.get_extension(CrossPackageExtToExtDependency, :ExtAB) isa Module || error("expected extension to load")
1218+
CrossPackageExtToExtDependency.greet()
1219+
"""
1220+
proj = joinpath(@__DIR__, "project", "Extensions", "CrossPackageExtToExtDependency")
1221+
cmd = `$(Base.julia_cmd()) --startup-file=no -e $code`
1222+
cmd = addenv(cmd,
1223+
"JULIA_LOAD_PATH" => proj,
1224+
"JULIA_DEPOT_PATH" => depot * Base.Filesystem.pathsep(),
1225+
)
1226+
@test occursin("Hello x-package ext-to-ext!", String(read(cmd)))
1227+
end
1228+
11611229
finally
11621230
try
11631231
rm(depot_path, force=true, recursive=true)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# This file is machine-generated - editing it directly is not advised
2+
3+
julia_version = "1.11.1"
4+
manifest_format = "2.0"
5+
project_hash = "dc35c2cf8c6b82fb5b9624c9713c2df34ca30499"
6+
7+
[[deps.CyclicExtensions]]
8+
deps = ["ExtDep"]
9+
path = "../CyclicExtensions"
10+
uuid = "17d4f0df-b55c-4714-ac4b-55fa23f7355c"
11+
version = "0.1.0"
12+
weakdeps = ["SomePackage"]
13+
14+
[deps.CyclicExtensions.extensions]
15+
ExtA = ["SomePackage"]
16+
ExtB = ["SomePackage"]
17+
18+
[[deps.ExtDep]]
19+
deps = ["SomeOtherPackage", "SomePackage"]
20+
path = "../ExtDep.jl"
21+
uuid = "fa069be4-f60b-4d4c-8b95-f8008775090c"
22+
version = "0.1.0"
23+
24+
[[deps.SomeOtherPackage]]
25+
path = "../SomeOtherPackage"
26+
uuid = "178f68a2-4498-45ee-a775-452b36359b63"
27+
version = "0.1.0"
28+
29+
[[deps.SomePackage]]
30+
path = "../SomePackage"
31+
uuid = "678608ae-7bb3-42c7-98b1-82102067a3d8"
32+
version = "0.1.0"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name = "CrossPackageExtToExtDependency"
2+
uuid = "30f07f2e-c47e-40db-93a2-cbc4d1b301cc"
3+
version = "0.1.0"
4+
5+
[deps]
6+
CyclicExtensions = "17d4f0df-b55c-4714-ac4b-55fa23f7355c"
7+
8+
[weakdeps]
9+
SomePackage = "678608ae-7bb3-42c7-98b1-82102067a3d8"
10+
11+
[extensions]
12+
ExtAB = ["CyclicExtensions", "SomePackage"]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module ExtAB
2+
3+
using CrossPackageExtToExtDependency
4+
using SomePackage
5+
using CyclicExtensions
6+
7+
const ExtA = Base.get_extension(CyclicExtensions, :ExtA)
8+
if !(ExtA isa Module)
9+
error("expected extension to load")
10+
end
11+
12+
end

0 commit comments

Comments
 (0)