Skip to content

Commit

Permalink
Relax restriction of preferences to top-level packages
Browse files Browse the repository at this point in the history
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
  • Loading branch information
staticfloat committed Nov 15, 2024
1 parent 100e305 commit 276d247
Showing 1 changed file with 64 additions and 18 deletions.
82 changes: 64 additions & 18 deletions base/loading.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3663,42 +3663,41 @@ end

# If we've asked for a specific UUID, this function will extract the prefs
# for that particular UUID. Otherwise, it returns all preferences.
function filter_preferences(prefs::Dict{String, Any}, pkg_name)
if pkg_name === nothing
function filter_preferences(toml_path::String, prefs::Dict{String, Any}, pkg_names::Set{String})
if isempty(pkg_names)
return prefs
else
return get(Dict{String, Any}, prefs, pkg_name)::Dict{String, Any}
present_pkg_names = filter(n -> n keys(prefs), pkg_names)
if length(present_pkg_names) > 1
@warn("""
$(toml_path) contains preference mappings that refer to the same UUID!
""", pkg_names=present_pkg_names)
end
if length(present_pkg_names) == 1
return prefs[only(present_pkg_names)]::Dict{String, Any}
end
return Dict{String,Any}()
end
end

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

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

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

# Next, look for `(Julia)LocalPreferences.toml` files next to this `Project.toml`
project_dir = dirname(project_toml)
for name in preferences_names
toml_path = joinpath(project_dir, name)
if isfile(toml_path)
prefs = parsed_toml(toml_path)
push!(dicts, filter_preferences(prefs, pkg_name))
push!(dicts, filter_preferences(toml_path, prefs, pkg_names))

# If we find `JuliaLocalPreferences.toml`, don't look for `LocalPreferences.toml`
break
Expand Down Expand Up @@ -3750,6 +3749,48 @@ function get_projects_workspace_to_root(project_file)
end
end

function build_uuid_name_map(envs::Vector{String})
uuid_map = Dict{UUID,Set{String}}()
name_map = Dict{String,UUID}()
disabled_names = Set{String}()
for env in envs
project_toml = env_project_file(env)
if !isa(project_toml, String)
continue
end

manifest_toml = project_file_manifest_path(project_toml)
if manifest_toml === nothing
continue
end
deps = get_deps(parsed_toml(manifest_toml))
for (name, entries) in deps
if name disabled_names
continue
end

uuid = UUID(only(entries)["uuid"])
# We keep the `name_map` just to ensure that our mapping of name -> uuid
# is unique, and therefore LocalPreferences.toml files are non-ambiguous
if get(name_map, name, uuid) != uuid
push!(disabled_names, name)
@warn("""
Two different UUIDs mapped to the same name in this environment!
Preferences are disabled for these packages due to this ambiguity.
""", name, uuid1=uuid, uuid2=name_map[name])
delete!(uuid_map, name_map[name])
else
if !haskey(uuid_map, uuid)
uuid_map[uuid] = Set{String}()
end
push!(uuid_map[uuid], name)
name_map[name] = uuid
end
end
end
return uuid_map
end

function get_preferences(uuid::Union{UUID,Nothing} = nothing)
merged_prefs = Dict{String,Any}()
loadpath = load_path()
Expand All @@ -3759,14 +3800,19 @@ function get_preferences(uuid::Union{UUID,Nothing} = nothing)
prepend!(projects_to_merge_prefs, get_projects_workspace_to_root(first(loadpath)))
end

# Build a mapping of UUIDs to names across our entire load path.
# This allows us to look up the name(s) of a UUID for a LocalPreferences.jl
# file that may not even have the package added as a direct dependency.
uuid_name_map = build_uuid_name_map(projects_to_merge_prefs)

for env in reverse(projects_to_merge_prefs)
project_toml = env_project_file(env)
if !isa(project_toml, String)
continue
end

# Collect all dictionaries from the current point in the load path, then merge them in
dicts = collect_preferences(project_toml, uuid)
dicts = collect_preferences(project_toml, uuid, uuid_name_map)
merged_prefs = recursive_prefs_merge(merged_prefs, dicts...)
end
return merged_prefs
Expand Down

0 comments on commit 276d247

Please sign in to comment.