Skip to content

Commit 7dfa7d1

Browse files
authored
loading: Try to clean up extension loading a bit (#59863)
The loading code is overall a bit messy. I'd like to add some new features to it, but I think before I can sensibly do that, some cleanup is required. This attempts to de-deduplicate the several places where extensions deal with weakdeps. The logic for extension loading weakdeps is present multiple times because we support loading extension: 1. By looking at the [extensions] section of the currently active Project.toml (for loading extensions of the currently active top level project) 2. By looking at the [extensions] sub-section of the top-level Manifest.toml (for any extension of a non-toplevel package); and 3. Like 1, except that rather than the env specifying a toplevel Project.toml, it specifies a directory and we search the second level for a Project.toml. This separation is a sensible resolution to the tradeoff of not wanting to scan all package's `Project.toml`s at load time while also not forcing a `resolve` for changes to the top-level Project.toml. Unfortunately, the loading behavior in case 1 currently differs from the loading behavior of case 2 and 3. In case 1, an extension is allowed to load any weakdep, in cases 2/3, it's only allowed to load weakdeps that are also extension triggers. I believe that the missing extra check for this restriction in case 1 is an oversight. So, this refactors all that by adding the check to case 1 and mostly deleting the code for case 3, by just putting the project-file-lookup code inline at the start of case 1. Additionally, I've factored out common queries into utility functions, even when the code itself is simple. The idea is to signal that these are the same data structure even if they appear in different places (Project vs Manifest). Lastly, this makes the following additional behavior changes that I don't expect to be observable: In case 2, when loading a non-extension-trigger, don't restart the search from scratch. The only case in which I could see this causing an observable behavior difference is if there are multiple versions of the package in the load path and for some reason the highest priority one doesn't have the extension (but then how did you load it in the first place??). However, even in that case, I think the new behavior is better, because it would get the dep of the version that *did* declare the extension.
1 parent d1b1d84 commit 7dfa7d1

File tree

2 files changed

+149
-139
lines changed

2 files changed

+149
-139
lines changed

base/loading.jl

Lines changed: 128 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -348,11 +348,14 @@ function identify_package_env(where::PkgId, name::String)
348348
else
349349
for env in load_path()
350350
pkgid = manifest_deps_get(env, where, name)
351-
pkgid === nothing && continue # not found--keep looking
351+
# If we didn't find `where` at all, keep looking through the environment stack
352+
pkgid === nothing && continue
352353
if pkgid.uuid !== nothing
353-
pkg_env = pkgid, env # found in explicit environment--use it
354+
pkg_env = pkgid, env
354355
end
355-
break # found in implicit environment--return "not found"
356+
# If we don't have pkgid.uuid, still break here - this is a sentinel that indicates
357+
# that we've found `where` but it did not have the required dependency. We terminate the search.
358+
break
356359
end
357360
if pkg_env === nothing && is_stdlib(where)
358361
# if not found it could be that manifests are from a different julia version/commit
@@ -723,51 +726,77 @@ function project_deps_get(env::String, name::String)::Union{Nothing,PkgId}
723726
return nothing
724727
end
725728

729+
function package_get_here(project_file, name::String)
730+
# if `where` matches the project, use [deps] section as manifest, and stop searching
731+
pkg_uuid = explicit_project_deps_get(project_file, name)
732+
pkg_uuid === nothing && return PkgId(name)
733+
return PkgId(pkg_uuid, name)
734+
end
735+
726736
function package_get(project_file, where::PkgId, name::String)
727737
proj = project_file_name_uuid(project_file, where.name)
728-
if proj == where
729-
# if `where` matches the project, use [deps] section as manifest, and stop searching
730-
pkg_uuid = explicit_project_deps_get(project_file, name)
731-
return PkgId(pkg_uuid, name)
732-
end
733-
return nothing
738+
proj != where && return nothing
739+
return package_get_here(project_file, name)
734740
end
735741

736-
function manifest_deps_get(env::String, where::PkgId, name::String)::Union{Nothing,PkgId}
737-
uuid = where.uuid
738-
@assert uuid !== nothing
739-
project_file = env_project_file(env)
740-
if project_file isa String
741-
pkg = package_get(project_file, where, name)
742-
pkg === nothing || return pkg
743-
d = parsed_toml(project_file)
744-
exts = get(d, "extensions", nothing)::Union{Dict{String, Any}, Nothing}
745-
if exts !== nothing
746-
proj = project_file_name_uuid(project_file, where.name)
747-
# Check if `where` is an extension of the project
748-
if where.name in keys(exts) && where.uuid == uuid5(proj.uuid::UUID, where.name)
749-
# Extensions can load weak deps...
742+
ext_may_load_weakdep(exts::String, name::String) = exts == name
743+
ext_may_load_weakdep(exts::Vector{String}, name::String) = name in exts
744+
745+
function package_extension_get(project_file, where::PkgId, name::String)
746+
d = parsed_toml(project_file)
747+
exts = get(d, "extensions", nothing)::Union{Dict{String, Any}, Nothing}
748+
if exts !== nothing
749+
proj = project_file_name_uuid(project_file, where.name)
750+
# Check if `where` is an extension of the project
751+
if where.name in keys(exts) && where.uuid == uuid5(proj.uuid::UUID, where.name)
752+
# Extensions can load weak deps if they are an extension trigger
753+
if ext_may_load_weakdep(exts[where.name]::Union{String, Vector{String}}, name)
750754
weakdeps = get(d, "weakdeps", nothing)::Union{Dict{String, Any}, Nothing}
751755
if weakdeps !== nothing
752756
wuuid = get(weakdeps, name, nothing)::Union{String, Nothing}
753757
if wuuid !== nothing
754758
return PkgId(UUID(wuuid), name)
755759
end
756760
end
757-
# ... and they can load same deps as the project itself
758-
mby_uuid = explicit_project_deps_get(project_file, name)
759-
mby_uuid === nothing || return PkgId(mby_uuid, name)
760761
end
762+
# ... and they can load same deps as the project itself
763+
return package_get_here(project_file, name)
761764
end
762-
# look for manifest file and `where` stanza
763-
return explicit_manifest_deps_get(project_file, where, name)
764-
elseif project_file
765-
# if env names a directory, search it
766-
return implicit_manifest_deps_get(env, where, name)
767765
end
768766
return nothing
769767
end
770768

769+
function manifest_deps_get(env::String, where::PkgId, name::String)::Union{Nothing,PkgId}
770+
@assert where.uuid !== nothing
771+
project_file = env_project_file(env)
772+
implicit_manifest = !(project_file isa String)
773+
if implicit_manifest
774+
project_file || return nothing
775+
project_file = implicit_manifest_project(env, where)
776+
project_file === nothing && return nothing
777+
end
778+
779+
# 1. Are we loading into the top-level project itself? dependencies come from [deps]
780+
# N.B.: Here "top-level" includes package loaded from an implicit manifest, which
781+
# uses the same code path.
782+
pkg = package_get(project_file, where, name)
783+
pkg === nothing || return pkg
784+
785+
# 2. Are we an extension of the top-level project? dependencies come from [weakdeps] and [deps]
786+
pkg = package_extension_get(project_file, where, name)
787+
pkg === nothing || return pkg
788+
789+
if implicit_manifest
790+
# With an implicit manifest, getting here means that our (implicit) environment
791+
# *has* the package `where`. If we don't find it, it just means that `where` doesn't
792+
# have `name` as a dependency - c.f. the analogous case in `explicit_manifest_deps_get`.
793+
return PkgId(name)
794+
end
795+
796+
# All other cases, dependencies come from the (top-level) manifest
797+
return explicit_manifest_deps_get(project_file, where, name)
798+
end
799+
771800
function manifest_uuid_path(env::String, pkg::PkgId)::Union{Nothing,String,Missing}
772801
project_file = env_project_file(env)
773802
if project_file isa String
@@ -938,7 +967,7 @@ end
938967
# find project file root or deps `name => uuid` mapping
939968
# `ext` is the name of the extension if `name` is loaded from one
940969
# return `nothing` if `name` is not found
941-
function explicit_project_deps_get(project_file::String, name::String, ext::Union{String,Nothing}=nothing)::Union{Nothing,UUID}
970+
function explicit_project_deps_get(project_file::String, name::String)::Union{Nothing,UUID}
942971
d = parsed_toml(project_file)
943972
if get(d, "name", nothing)::Union{String, Nothing} === name
944973
root_uuid = dummy_uuid(project_file)
@@ -950,19 +979,6 @@ function explicit_project_deps_get(project_file::String, name::String, ext::Unio
950979
uuid = get(deps, name, nothing)::Union{String, Nothing}
951980
uuid === nothing || return UUID(uuid)
952981
end
953-
if ext !== nothing
954-
extensions = get(d, "extensions", nothing)
955-
extensions === nothing && return nothing
956-
ext_data = get(extensions, ext, nothing)
957-
ext_data === nothing && return nothing
958-
if (ext_data isa String && name == ext_data) || (ext_data isa Vector{String} && name in ext_data)
959-
weakdeps = get(d, "weakdeps", nothing)::Union{Dict{String, Any}, Nothing}
960-
weakdeps === nothing && return nothing
961-
wuuid = get(weakdeps, name, nothing)::Union{String, Nothing}
962-
wuuid === nothing && return nothing
963-
return UUID(wuuid)
964-
end
965-
end
966982
return nothing
967983
end
968984

@@ -989,14 +1005,27 @@ function get_deps(raw_manifest::Dict)
9891005
end
9901006
end
9911007

992-
# find `where` stanza and return the PkgId for `name`
993-
# return `nothing` if it did not find `where` (indicating caller should continue searching)
1008+
function dep_stanza_get(stanza::Dict{String, Any}, name::String)::Union{Nothing, PkgId}
1009+
for (dep, uuid) in stanza
1010+
uuid::String
1011+
if dep === name
1012+
return PkgId(UUID(uuid), name)
1013+
end
1014+
end
1015+
return nothing
1016+
end
1017+
1018+
function dep_stanza_get(stanza::Vector{String}, name::String)::Union{Nothing, PkgId}
1019+
name in stanza && return PkgId(name)
1020+
return nothing
1021+
end
1022+
1023+
dep_stanza_get(stanza::Nothing, name::String) = nothing
1024+
9941025
function explicit_manifest_deps_get(project_file::String, where::PkgId, name::String)::Union{Nothing,PkgId}
9951026
manifest_file = project_file_manifest_path(project_file)
9961027
manifest_file === nothing && return nothing # manifest not found--keep searching LOAD_PATH
9971028
d = get_deps(parsed_toml(manifest_file))
998-
found_where = false
999-
found_name = false
10001029
for (dep_name, entries) in d
10011030
entries::Vector{Any}
10021031
for entry in entries
@@ -1006,67 +1035,62 @@ function explicit_manifest_deps_get(project_file::String, where::PkgId, name::St
10061035
# deps is either a list of names (deps = ["DepA", "DepB"]) or
10071036
# a table of entries (deps = {"DepA" = "6ea...", "DepB" = "55d..."}
10081037
deps = get(entry, "deps", nothing)::Union{Vector{String}, Dict{String, Any}, Nothing}
1038+
local dep::Union{Nothing, PkgId}
10091039
if UUID(uuid) === where.uuid
1010-
found_where = true
1011-
if deps isa Vector{String}
1012-
found_name = name in deps
1013-
found_name && @goto done
1014-
elseif deps isa Dict{String, Any}
1015-
deps = deps::Dict{String, Any}
1016-
for (dep, uuid) in deps
1017-
uuid::String
1018-
if dep === name
1019-
return PkgId(UUID(uuid), name)
1020-
end
1021-
end
1022-
end
1023-
else # Check for extensions
1040+
dep = dep_stanza_get(deps, name)
1041+
1042+
# We found `where` in this environment, but it did not have a deps entry for
1043+
# `name`. This is likely because the dependency was modified without a corresponding
1044+
# change to dependency's Project or our Manifest. Return a sentinel here indicating
1045+
# that we know the package, but do not know its UUID. The caller will terminate the
1046+
# search and provide an appropriate error to the user.
1047+
dep === nothing && return PkgId(name)
1048+
else
1049+
# Check if we're trying to load into an extension of this package
10241050
extensions = get(entry, "extensions", nothing)
10251051
if extensions !== nothing
10261052
if haskey(extensions, where.name) && where.uuid == uuid5(UUID(uuid), where.name)
1027-
found_where = true
10281053
if name == dep_name
1054+
# Extension loads its base package
10291055
return PkgId(UUID(uuid), name)
10301056
end
10311057
exts = extensions[where.name]::Union{String, Vector{String}}
1032-
weakdeps = get(entry, "weakdeps", nothing)::Union{Vector{String}, Dict{String, Any}, Nothing}
1033-
if (exts isa String && name == exts) || (exts isa Vector{String} && name in exts)
1034-
for deps′ in [weakdeps, deps]
1035-
if deps′ !== nothing
1036-
if deps′ isa Vector{String}
1037-
found_name = name in deps′
1038-
found_name && @goto done
1039-
elseif deps′ isa Dict{String, Any}
1040-
deps′ = deps′::Dict{String, Any}
1041-
for (dep, uuid) in deps′
1042-
uuid::String
1043-
if dep === name
1044-
return PkgId(UUID(uuid), name)
1045-
end
1046-
end
1047-
end
1048-
end
1049-
end
1050-
end
1051-
# `name` is not an ext, do standard lookup as if this was the parent
1052-
return identify_package(PkgId(UUID(uuid), dep_name), name)
1058+
# Extensions are allowed to load:
1059+
# 1. Any ordinary dep of the parent package
1060+
# 2. Any weakdep of the parent package declared as an extension trigger
1061+
for deps′ in (ext_may_load_weakdep(exts, name) ?
1062+
(get(entry, "weakdeps", nothing)::Union{Vector{String}, Dict{String, Any}, Nothing}, deps) :
1063+
(deps,))
1064+
dep = dep_stanza_get(deps′, name)
1065+
dep === nothing && continue
1066+
@goto have_dep
1067+
end
1068+
return PkgId(name)
10531069
end
10541070
end
1071+
continue
10551072
end
1073+
1074+
@label have_dep
1075+
dep.uuid !== nothing && return dep
1076+
1077+
# We have the dep, but it did not specify a UUID. In this case,
1078+
# it must be that the name is unique in the manifest - so lookup
1079+
# the UUID at the lop level by name
1080+
name_deps = get(d, name, nothing)::Union{Nothing, Vector{Any}}
1081+
if name_deps === nothing || length(name_deps) != 1
1082+
error("expected a single entry for $(repr(name)) in $(repr(project_file))")
1083+
end
1084+
entry = first(name_deps::Vector{Any})::Dict{String, Any}
1085+
uuid = get(entry, "uuid", nothing)::Union{String, Nothing}
1086+
uuid === nothing && return PkgId(name)
1087+
return PkgId(UUID(uuid), name)
10561088
end
10571089
end
1058-
@label done
1059-
found_where || return nothing
1060-
found_name || return PkgId(name)
1061-
# Only reach here if deps was not a dict which mean we have a unique name for the dep
1062-
name_deps = get(d, name, nothing)::Union{Nothing, Vector{Any}}
1063-
if name_deps === nothing || length(name_deps) != 1
1064-
error("expected a single entry for $(repr(name)) in $(repr(project_file))")
1065-
end
1066-
entry = first(name_deps::Vector{Any})::Dict{String, Any}
1067-
uuid = get(entry, "uuid", nothing)::Union{String, Nothing}
1068-
uuid === nothing && return nothing
1069-
return PkgId(UUID(uuid), name)
1090+
1091+
# We did not find `where` in this environment, either as a package or as an extension.
1092+
# The caller should continue searching the environment stack.
1093+
return nothing
10701094
end
10711095

10721096
# find `uuid` stanza, return the corresponding path
@@ -1149,35 +1173,16 @@ function implicit_project_deps_get(dir::String, name::String)::Union{Nothing,Pkg
11491173
return proj
11501174
end
11511175

1152-
# look for an entry-point for `name`, check that UUID matches
1153-
# if there's a project file, look up `name` in its deps and return that
1154-
# otherwise return `nothing` to indicate the caller should keep searching
1155-
function implicit_manifest_deps_get(dir::String, where::PkgId, name::String)::Union{Nothing,PkgId}
1156-
@assert where.uuid !== nothing
1157-
project_file = entry_point_and_project_file(dir, where.name)[2]
1176+
function implicit_manifest_project(dir, pkg::PkgId)::Union{Nothing, String}
1177+
@assert pkg.uuid !== nothing
1178+
project_file = entry_point_and_project_file(dir, pkg.name)[2]
11581179
if project_file === nothing
11591180
# `where` could be an extension
1160-
project_file = implicit_env_project_file_extension(dir, where)[2]
1161-
project_file === nothing && return nothing
1162-
end
1163-
proj = project_file_name_uuid(project_file, where.name)
1164-
ext = nothing
1165-
if proj !== where
1166-
# `where` could be an extension in `proj`
1167-
d = parsed_toml(project_file)
1168-
exts = get(d, "extensions", nothing)::Union{Dict{String, Any}, Nothing}
1169-
if exts !== nothing && where.name in keys(exts)
1170-
if where.uuid !== uuid5(proj.uuid, where.name)
1171-
return nothing
1172-
end
1173-
ext = where.name
1174-
else
1175-
return nothing
1176-
end
1181+
return implicit_env_project_file_extension(dir, pkg)[2]
11771182
end
1178-
# this is the correct project, so stop searching here
1179-
pkg_uuid = explicit_project_deps_get(project_file, name, ext)
1180-
return PkgId(pkg_uuid, name)
1183+
proj = project_file_name_uuid(project_file, pkg.name)
1184+
proj == pkg || return nothing
1185+
return project_file
11811186
end
11821187

11831188
# look for an entry-point for `pkg` and return its path if UUID matches

0 commit comments

Comments
 (0)