Skip to content

Commit 34e6520

Browse files
committed
loading: Try to clean up extension loading a bit
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 9cddfda commit 34e6520

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
@@ -698,51 +701,77 @@ function project_deps_get(env::String, name::String)::Union{Nothing,PkgId}
698701
return nothing
699702
end
700703

704+
function package_get_here(project_file, name::String)
705+
# if `where` matches the project, use [deps] section as manifest, and stop searching
706+
pkg_uuid = explicit_project_deps_get(project_file, name)
707+
pkg_uuid === nothing && return PkgId(name)
708+
return PkgId(pkg_uuid, name)
709+
end
710+
701711
function package_get(project_file, where::PkgId, name::String)
702712
proj = project_file_name_uuid(project_file, where.name)
703-
if proj == where
704-
# if `where` matches the project, use [deps] section as manifest, and stop searching
705-
pkg_uuid = explicit_project_deps_get(project_file, name)
706-
return PkgId(pkg_uuid, name)
707-
end
708-
return nothing
713+
proj != where && return nothing
714+
return package_get_here(project_file, name)
709715
end
710716

711-
function manifest_deps_get(env::String, where::PkgId, name::String)::Union{Nothing,PkgId}
712-
uuid = where.uuid
713-
@assert uuid !== nothing
714-
project_file = env_project_file(env)
715-
if project_file isa String
716-
pkg = package_get(project_file, where, name)
717-
pkg === nothing || return pkg
718-
d = parsed_toml(project_file)
719-
exts = get(d, "extensions", nothing)::Union{Dict{String, Any}, Nothing}
720-
if exts !== nothing
721-
proj = project_file_name_uuid(project_file, where.name)
722-
# Check if `where` is an extension of the project
723-
if where.name in keys(exts) && where.uuid == uuid5(proj.uuid::UUID, where.name)
724-
# Extensions can load weak deps...
717+
ext_may_load_weakdep(exts::String, name::String) = exts == name
718+
ext_may_load_weakdep(exts::Vector{String}, name::String) = name in exts
719+
720+
function package_extension_get(project_file, where::PkgId, name::String)
721+
d = parsed_toml(project_file)
722+
exts = get(d, "extensions", nothing)::Union{Dict{String, Any}, Nothing}
723+
if exts !== nothing
724+
proj = project_file_name_uuid(project_file, where.name)
725+
# Check if `where` is an extension of the project
726+
if where.name in keys(exts) && where.uuid == uuid5(proj.uuid::UUID, where.name)
727+
# Extensions can load weak deps if they are an extension trigger
728+
if ext_may_load_weakdep(exts[where.name]::Union{String, Vector{String}}, name)
725729
weakdeps = get(d, "weakdeps", nothing)::Union{Dict{String, Any}, Nothing}
726730
if weakdeps !== nothing
727731
wuuid = get(weakdeps, name, nothing)::Union{String, Nothing}
728732
if wuuid !== nothing
729733
return PkgId(UUID(wuuid), name)
730734
end
731735
end
732-
# ... and they can load same deps as the project itself
733-
mby_uuid = explicit_project_deps_get(project_file, name)
734-
mby_uuid === nothing || return PkgId(mby_uuid, name)
735736
end
737+
# ... and they can load same deps as the project itself
738+
return package_get_here(project_file, name)
736739
end
737-
# look for manifest file and `where` stanza
738-
return explicit_manifest_deps_get(project_file, where, name)
739-
elseif project_file
740-
# if env names a directory, search it
741-
return implicit_manifest_deps_get(env, where, name)
742740
end
743741
return nothing
744742
end
745743

744+
function manifest_deps_get(env::String, where::PkgId, name::String)::Union{Nothing,PkgId}
745+
@assert where.uuid !== nothing
746+
project_file = env_project_file(env)
747+
implicit_manifest = !(project_file isa String)
748+
if implicit_manifest
749+
project_file || return nothing
750+
project_file = implicit_manifest_project(env, where)
751+
project_file === nothing && return nothing
752+
end
753+
754+
# 1. Are we loading into the top-level project itself? dependencies come from [deps]
755+
# N.B.: Here "top-level" includes package loaded from an implicit manifest, which
756+
# uses the same code path.
757+
pkg = package_get(project_file, where, name)
758+
pkg === nothing || return pkg
759+
760+
# 2. Are we an extension of the top-level project? dependencies come from [weakdeps] and [deps]
761+
pkg = package_extension_get(project_file, where, name)
762+
pkg === nothing || return pkg
763+
764+
if implicit_manifest
765+
# With an implicit manifest, getting here means that our (implicit) environment
766+
# *has* the package `where`. If we don't find it, it just means that `where` doesn't
767+
# have `name` as a dependency - c.f. the analogous case in `explicit_manifest_deps_get`.
768+
return PkgId(name)
769+
end
770+
771+
# All other cases, dependencies come from the (top-level) manifest
772+
return explicit_manifest_deps_get(project_file, where, name)
773+
end
774+
746775
function manifest_uuid_path(env::String, pkg::PkgId)::Union{Nothing,String,Missing}
747776
project_file = env_project_file(env)
748777
if project_file isa String
@@ -913,7 +942,7 @@ end
913942
# find project file root or deps `name => uuid` mapping
914943
# `ext` is the name of the extension if `name` is loaded from one
915944
# return `nothing` if `name` is not found
916-
function explicit_project_deps_get(project_file::String, name::String, ext::Union{String,Nothing}=nothing)::Union{Nothing,UUID}
945+
function explicit_project_deps_get(project_file::String, name::String)::Union{Nothing,UUID}
917946
d = parsed_toml(project_file)
918947
if get(d, "name", nothing)::Union{String, Nothing} === name
919948
root_uuid = dummy_uuid(project_file)
@@ -925,19 +954,6 @@ function explicit_project_deps_get(project_file::String, name::String, ext::Unio
925954
uuid = get(deps, name, nothing)::Union{String, Nothing}
926955
uuid === nothing || return UUID(uuid)
927956
end
928-
if ext !== nothing
929-
extensions = get(d, "extensions", nothing)
930-
extensions === nothing && return nothing
931-
ext_data = get(extensions, ext, nothing)
932-
ext_data === nothing && return nothing
933-
if (ext_data isa String && name == ext_data) || (ext_data isa Vector{String} && name in ext_data)
934-
weakdeps = get(d, "weakdeps", nothing)::Union{Dict{String, Any}, Nothing}
935-
weakdeps === nothing && return nothing
936-
wuuid = get(weakdeps, name, nothing)::Union{String, Nothing}
937-
wuuid === nothing && return nothing
938-
return UUID(wuuid)
939-
end
940-
end
941957
return nothing
942958
end
943959

@@ -964,14 +980,27 @@ function get_deps(raw_manifest::Dict)
964980
end
965981
end
966982

967-
# find `where` stanza and return the PkgId for `name`
968-
# return `nothing` if it did not find `where` (indicating caller should continue searching)
983+
function dep_stanza_get(stanza::Dict{String, Any}, name::String)::Union{Nothing, PkgId}
984+
for (dep, uuid) in stanza
985+
uuid::String
986+
if dep === name
987+
return PkgId(UUID(uuid), name)
988+
end
989+
end
990+
return nothing
991+
end
992+
993+
function dep_stanza_get(stanza::Vector{String}, name::String)::Union{Nothing, PkgId}
994+
name in stanza && return PkgId(name)
995+
return nothing
996+
end
997+
998+
dep_stanza_get(stanza::Nothing, name::String) = nothing
999+
9691000
function explicit_manifest_deps_get(project_file::String, where::PkgId, name::String)::Union{Nothing,PkgId}
9701001
manifest_file = project_file_manifest_path(project_file)
9711002
manifest_file === nothing && return nothing # manifest not found--keep searching LOAD_PATH
9721003
d = get_deps(parsed_toml(manifest_file))
973-
found_where = false
974-
found_name = false
9751004
for (dep_name, entries) in d
9761005
entries::Vector{Any}
9771006
for entry in entries
@@ -981,67 +1010,62 @@ function explicit_manifest_deps_get(project_file::String, where::PkgId, name::St
9811010
# deps is either a list of names (deps = ["DepA", "DepB"]) or
9821011
# a table of entries (deps = {"DepA" = "6ea...", "DepB" = "55d..."}
9831012
deps = get(entry, "deps", nothing)::Union{Vector{String}, Dict{String, Any}, Nothing}
1013+
local dep::Union{Nothing, PkgId}
9841014
if UUID(uuid) === where.uuid
985-
found_where = true
986-
if deps isa Vector{String}
987-
found_name = name in deps
988-
found_name && @goto done
989-
elseif deps isa Dict{String, Any}
990-
deps = deps::Dict{String, Any}
991-
for (dep, uuid) in deps
992-
uuid::String
993-
if dep === name
994-
return PkgId(UUID(uuid), name)
995-
end
996-
end
997-
end
998-
else # Check for extensions
1015+
dep = dep_stanza_get(deps, name)
1016+
1017+
# We found `where` in this environment, but it did not have a deps entry for
1018+
# `name`. This is likely because the dependency was modified without a corresponding
1019+
# change to dependency's Project or our Manifest. Return a sentinel here indicating
1020+
# that we know the package, but do not know its UUID. The caller will terminate the
1021+
# search and provide an appropriate error to the user.
1022+
dep === nothing && return PkgId(name)
1023+
else
1024+
# Check if we're trying to load into an extension of this package
9991025
extensions = get(entry, "extensions", nothing)
10001026
if extensions !== nothing
10011027
if haskey(extensions, where.name) && where.uuid == uuid5(UUID(uuid), where.name)
1002-
found_where = true
10031028
if name == dep_name
1029+
# Extension loads its base package
10041030
return PkgId(UUID(uuid), name)
10051031
end
10061032
exts = extensions[where.name]::Union{String, Vector{String}}
1007-
weakdeps = get(entry, "weakdeps", nothing)::Union{Vector{String}, Dict{String, Any}, Nothing}
1008-
if (exts isa String && name == exts) || (exts isa Vector{String} && name in exts)
1009-
for deps′ in [weakdeps, deps]
1010-
if deps′ !== nothing
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-
end
1024-
end
1025-
end
1026-
# `name` is not an ext, do standard lookup as if this was the parent
1027-
return identify_package(PkgId(UUID(uuid), dep_name), name)
1033+
# Extensions are allowed to load:
1034+
# 1. Any ordinary dep of the parent package
1035+
# 2. Any weakdep of the parent package declared as an extension trigger
1036+
for deps′ in (ext_may_load_weakdep(exts, name) ?
1037+
(get(entry, "weakdeps", nothing)::Union{Vector{String}, Dict{String, Any}, Nothing}, deps) :
1038+
(deps,))
1039+
dep = dep_stanza_get(deps′, name)
1040+
dep === nothing && continue
1041+
@goto have_dep
1042+
end
1043+
return PkgId(name)
10281044
end
10291045
end
1046+
continue
10301047
end
1048+
1049+
@label have_dep
1050+
dep.uuid !== nothing && return dep
1051+
1052+
# We have the dep, but it did not specify a UUID. In this case,
1053+
# it must be that the name is unique in the manifest - so lookup
1054+
# the UUID at the lop level by name
1055+
name_deps = get(d, name, nothing)::Union{Nothing, Vector{Any}}
1056+
if name_deps === nothing || length(name_deps) != 1
1057+
error("expected a single entry for $(repr(name)) in $(repr(project_file))")
1058+
end
1059+
entry = first(name_deps::Vector{Any})::Dict{String, Any}
1060+
uuid = get(entry, "uuid", nothing)::Union{String, Nothing}
1061+
uuid === nothing && return PkgId(name)
1062+
return PkgId(UUID(uuid), name)
10311063
end
10321064
end
1033-
@label done
1034-
found_where || return nothing
1035-
found_name || return PkgId(name)
1036-
# Only reach here if deps was not a dict which mean we have a unique name for the dep
1037-
name_deps = get(d, name, nothing)::Union{Nothing, Vector{Any}}
1038-
if name_deps === nothing || length(name_deps) != 1
1039-
error("expected a single entry for $(repr(name)) in $(repr(project_file))")
1040-
end
1041-
entry = first(name_deps::Vector{Any})::Dict{String, Any}
1042-
uuid = get(entry, "uuid", nothing)::Union{String, Nothing}
1043-
uuid === nothing && return nothing
1044-
return PkgId(UUID(uuid), name)
1065+
1066+
# We did not find `where` in this environment, either as a package or as an extension.
1067+
# The caller should continue searching the environment stack.
1068+
return nothing
10451069
end
10461070

10471071
# find `uuid` stanza, return the corresponding path
@@ -1124,35 +1148,16 @@ function implicit_project_deps_get(dir::String, name::String)::Union{Nothing,Pkg
11241148
return proj
11251149
end
11261150

1127-
# look for an entry-point for `name`, check that UUID matches
1128-
# if there's a project file, look up `name` in its deps and return that
1129-
# otherwise return `nothing` to indicate the caller should keep searching
1130-
function implicit_manifest_deps_get(dir::String, where::PkgId, name::String)::Union{Nothing,PkgId}
1131-
@assert where.uuid !== nothing
1132-
project_file = entry_point_and_project_file(dir, where.name)[2]
1151+
function implicit_manifest_project(dir, pkg::PkgId)::Union{Nothing, String}
1152+
@assert pkg.uuid !== nothing
1153+
project_file = entry_point_and_project_file(dir, pkg.name)[2]
11331154
if project_file === nothing
11341155
# `where` could be an extension
1135-
project_file = implicit_env_project_file_extension(dir, where)[2]
1136-
project_file === nothing && return nothing
1137-
end
1138-
proj = project_file_name_uuid(project_file, where.name)
1139-
ext = nothing
1140-
if proj !== where
1141-
# `where` could be an extension in `proj`
1142-
d = parsed_toml(project_file)
1143-
exts = get(d, "extensions", nothing)::Union{Dict{String, Any}, Nothing}
1144-
if exts !== nothing && where.name in keys(exts)
1145-
if where.uuid !== uuid5(proj.uuid, where.name)
1146-
return nothing
1147-
end
1148-
ext = where.name
1149-
else
1150-
return nothing
1151-
end
1156+
return implicit_env_project_file_extension(dir, pkg)[2]
11521157
end
1153-
# this is the correct project, so stop searching here
1154-
pkg_uuid = explicit_project_deps_get(project_file, name, ext)
1155-
return PkgId(pkg_uuid, name)
1158+
proj = project_file_name_uuid(project_file, pkg.name)
1159+
proj == pkg || return nothing
1160+
return project_file
11561161
end
11571162

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

0 commit comments

Comments
 (0)