From 276d2475c41850b5cf37753f2cb61b72cff735a7 Mon Sep 17 00:00:00 2001 From: Elliot Saba Date: Fri, 15 Nov 2024 10:50:37 -0800 Subject: [PATCH] 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] https://github.com/JuliaLang/julia/pull/37595 [1] https://github.com/JuliaLang/julia/pull/38044 [2] https://github.com/JuliaPackaging/Preferences.jl/issues/33 --- base/loading.jl | 82 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 18 deletions(-) diff --git a/base/loading.jl b/base/loading.jl index 79b4fb8cb9fcc..663aa676bcee1 100644 --- a/base/loading.jl +++ b/base/loading.jl @@ -3663,34 +3663,33 @@ 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) @@ -3698,7 +3697,7 @@ function collect_preferences(project_toml::String, uuid::Union{UUID,Nothing}) 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 @@ -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() @@ -3759,6 +3800,11 @@ 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) @@ -3766,7 +3812,7 @@ function get_preferences(uuid::Union{UUID,Nothing} = nothing) 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