Skip to content

Commit b6999d3

Browse files
committed
Relax restriction of preferences to top-level packages
When preferences were first added, we originally did not have any preference merging across load path [0]. We later added that [1], but retained the requirement that for each individual element in the load path, preferences must have an entry in `Project.toml` listing the relevant package. This was partly on purpose (immediately binding the name to the UUID prevents confusion when a UUID<->name binding is not readily apparent) and partly just inheriting the way things worked back when preferences was written with just a single Project.toml in mind. This PR breaks this assumption to specifically allow an entry in the Julia load path that contains only a single `LocalPreferences.toml` file that sets preferences for packages that may or may not be in the current environment stack. The usecase and desire for this is well-motivated in [2], but basically boils down to "system admin provided preferences that should be used globally". In fact, one such convenient method that now works is to drop a `LocalPreferences.toml` file in the `stdlib` directory of your Julia distribution, as that is almost always a part of a Julia process's load path. [0] #37595 [1] #38044 [2] JuliaPackaging/Preferences.jl#33
1 parent 989973a commit b6999d3

File tree

1 file changed

+64
-18
lines changed

1 file changed

+64
-18
lines changed

base/loading.jl

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3679,42 +3679,41 @@ end
36793679

36803680
# If we've asked for a specific UUID, this function will extract the prefs
36813681
# for that particular UUID. Otherwise, it returns all preferences.
3682-
function filter_preferences(prefs::Dict{String, Any}, pkg_name)
3683-
if pkg_name === nothing
3682+
function filter_preferences(toml_path::String, prefs::Dict{String, Any}, pkg_names::Set{String})
3683+
if isempty(pkg_names)
36843684
return prefs
36853685
else
3686-
return get(Dict{String, Any}, prefs, pkg_name)::Dict{String, Any}
3686+
present_pkg_names = filter(n -> n keys(prefs), pkg_names)
3687+
if length(present_pkg_names) > 1
3688+
@warn("""
3689+
$(toml_path) contains preference mappings that refer to the same UUID!
3690+
""", pkg_names=present_pkg_names)
3691+
end
3692+
if length(present_pkg_names) == 1
3693+
return prefs[only(present_pkg_names)]::Dict{String, Any}
3694+
end
3695+
return Dict{String,Any}()
36873696
end
36883697
end
36893698

3690-
function collect_preferences(project_toml::String, uuid::Union{UUID,Nothing})
3699+
function collect_preferences(project_toml::String, uuid::Union{UUID,Nothing}, uuid_name_map::Dict{UUID,Set{String}})
36913700
# We'll return a list of dicts to be merged
36923701
dicts = Dict{String, Any}[]
36933702

36943703
project = parsed_toml(project_toml)
3695-
pkg_name = nothing
3696-
if uuid !== nothing
3697-
# If we've been given a UUID, map that to the name of the package as
3698-
# recorded in the preferences section. If we can't find that mapping,
3699-
# exit out, as it means there's no way preferences can be set for that
3700-
# UUID, as we only allow actual dependencies to have preferences set.
3701-
pkg_name = get_uuid_name(project, uuid)
3702-
if pkg_name === nothing
3703-
return dicts
3704-
end
3705-
end
3704+
pkg_names = get(Set{String}, uuid_name_map, uuid)::Set{String}
37063705

37073706
# Look first inside of `Project.toml` to see we have preferences embedded within there
37083707
proj_preferences = get(Dict{String, Any}, project, "preferences")::Dict{String, Any}
3709-
push!(dicts, filter_preferences(proj_preferences, pkg_name))
3708+
push!(dicts, filter_preferences(project_toml, proj_preferences, pkg_names))
37103709

37113710
# Next, look for `(Julia)LocalPreferences.toml` files next to this `Project.toml`
37123711
project_dir = dirname(project_toml)
37133712
for name in preferences_names
37143713
toml_path = joinpath(project_dir, name)
37153714
if isfile(toml_path)
37163715
prefs = parsed_toml(toml_path)
3717-
push!(dicts, filter_preferences(prefs, pkg_name))
3716+
push!(dicts, filter_preferences(toml_path, prefs, pkg_names))
37183717

37193718
# If we find `JuliaLocalPreferences.toml`, don't look for `LocalPreferences.toml`
37203719
break
@@ -3766,6 +3765,48 @@ function get_projects_workspace_to_root(project_file)
37663765
end
37673766
end
37683767

3768+
function build_uuid_name_map(envs::Vector{String})
3769+
uuid_map = Dict{UUID,Set{String}}()
3770+
name_map = Dict{String,UUID}()
3771+
disabled_names = Set{String}()
3772+
for env in envs
3773+
project_toml = env_project_file(env)
3774+
if !isa(project_toml, String)
3775+
continue
3776+
end
3777+
3778+
manifest_toml = project_file_manifest_path(project_toml)
3779+
if manifest_toml === nothing
3780+
continue
3781+
end
3782+
deps = get_deps(parsed_toml(manifest_toml))
3783+
for (name, entries) in deps
3784+
if name disabled_names
3785+
continue
3786+
end
3787+
3788+
uuid = UUID(only(entries)["uuid"])
3789+
# We keep the `name_map` just to ensure that our mapping of name -> uuid
3790+
# is unique, and therefore LocalPreferences.toml files are non-ambiguous
3791+
if get(name_map, name, uuid) != uuid
3792+
push!(disabled_names, name)
3793+
@warn("""
3794+
Two different UUIDs mapped to the same name in this environment!
3795+
Preferences are disabled for these packages due to this ambiguity.
3796+
""", name, uuid1=uuid, uuid2=name_map[name])
3797+
delete!(uuid_map, name_map[name])
3798+
else
3799+
if !haskey(uuid_map, uuid)
3800+
uuid_map[uuid] = Set{String}()
3801+
end
3802+
push!(uuid_map[uuid], name)
3803+
name_map[name] = uuid
3804+
end
3805+
end
3806+
end
3807+
return uuid_map
3808+
end
3809+
37693810
function get_preferences(uuid::Union{UUID,Nothing} = nothing)
37703811
merged_prefs = Dict{String,Any}()
37713812
loadpath = load_path()
@@ -3775,14 +3816,19 @@ function get_preferences(uuid::Union{UUID,Nothing} = nothing)
37753816
prepend!(projects_to_merge_prefs, get_projects_workspace_to_root(first(loadpath)))
37763817
end
37773818

3819+
# Build a mapping of UUIDs to names across our entire load path.
3820+
# This allows us to look up the name(s) of a UUID for a LocalPreferences.jl
3821+
# file that may not even have the package added as a direct dependency.
3822+
uuid_name_map = build_uuid_name_map(projects_to_merge_prefs)
3823+
37783824
for env in reverse(projects_to_merge_prefs)
37793825
project_toml = env_project_file(env)
37803826
if !isa(project_toml, String)
37813827
continue
37823828
end
37833829

37843830
# Collect all dictionaries from the current point in the load path, then merge them in
3785-
dicts = collect_preferences(project_toml, uuid)
3831+
dicts = collect_preferences(project_toml, uuid, uuid_name_map)
37863832
merged_prefs = recursive_prefs_merge(merged_prefs, dicts...)
37873833
end
37883834
return merged_prefs

0 commit comments

Comments
 (0)