Skip to content

Commit 94efe75

Browse files
committed
Add Preferences standard library
This commit adds the `Preferences` standard library; a way to store a TOML-serializable dictionary into top-level `Project.toml` files, then force recompilation of child projects when the preferences are modified. This pull request adds the `Preferences` standard library, which does the actual writing to `Project.toml` files, as well as modifies the loading code to check whether the preferences have changed.
1 parent 8bdf569 commit 94efe75

File tree

13 files changed

+466
-8
lines changed

13 files changed

+466
-8
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ Standard library changes
105105
* The `Pkg.Artifacts` module has been imported as a separate standard library. It is still available as
106106
`Pkg.Artifacts`, however starting from Julia v1.6+, packages may import simply `Artifacts` without importing
107107
all of `Pkg` alongside. ([#37320])
108+
* A new standard library, `Preferences`, has been added to allow packages to store settings within the top-
109+
level `Project.toml`, and force recompilation when the preferences are changed. ([#xxxxx])
108110

109111
#### LinearAlgebra
110112
* New method `LinearAlgebra.issuccess(::CholeskyPivoted)` for checking whether pivoted Cholesky factorization was successful ([#36002]).

base/loading.jl

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,6 +1316,8 @@ function parse_cache_header(f::IO)
13161316
end
13171317
totbytes -= 4 + 4 + n2 + 8
13181318
end
1319+
prefs_hash = read(f, UInt64)
1320+
totbytes -= 8
13191321
@assert totbytes == 12 "header of cache file appears to be corrupt"
13201322
srctextpos = read(f, Int64)
13211323
# read the list of modules that are required to be present during loading
@@ -1328,7 +1330,7 @@ function parse_cache_header(f::IO)
13281330
build_id = read(f, UInt64) # build id
13291331
push!(required_modules, PkgId(uuid, sym) => build_id)
13301332
end
1331-
return modules, (includes, requires), required_modules, srctextpos
1333+
return modules, (includes, requires), required_modules, srctextpos, prefs_hash
13321334
end
13331335

13341336
function parse_cache_header(cachefile::String; srcfiles_only::Bool=false)
@@ -1337,21 +1339,21 @@ function parse_cache_header(cachefile::String; srcfiles_only::Bool=false)
13371339
!isvalid_cache_header(io) && throw(ArgumentError("Invalid header in cache file $cachefile."))
13381340
ret = parse_cache_header(io)
13391341
srcfiles_only || return ret
1340-
modules, (includes, requires), required_modules, srctextpos = ret
1342+
modules, (includes, requires), required_modules, srctextpos, prefs_hash = ret
13411343
srcfiles = srctext_files(io, srctextpos)
13421344
delidx = Int[]
13431345
for (i, chi) in enumerate(includes)
13441346
chi.filename srcfiles || push!(delidx, i)
13451347
end
13461348
deleteat!(includes, delidx)
1347-
return modules, (includes, requires), required_modules, srctextpos
1349+
return modules, (includes, requires), required_modules, srctextpos, prefs_hash
13481350
finally
13491351
close(io)
13501352
end
13511353
end
13521354

13531355
function cache_dependencies(f::IO)
1354-
defs, (includes, requires), modules = parse_cache_header(f)
1356+
defs, (includes, requires), modules, srctextpos, prefs_hash = parse_cache_header(f)
13551357
return modules, map(chi -> (chi.filename, chi.mtime), includes) # return just filename and mtime
13561358
end
13571359

@@ -1366,7 +1368,7 @@ function cache_dependencies(cachefile::String)
13661368
end
13671369

13681370
function read_dependency_src(io::IO, filename::AbstractString)
1369-
modules, (includes, requires), required_modules, srctextpos = parse_cache_header(io)
1371+
modules, (includes, requires), required_modules, srctextpos, prefs_hash = parse_cache_header(io)
13701372
srctextpos == 0 && error("no source-text stored in cache file")
13711373
seek(io, srctextpos)
13721374
return _read_dependency_src(io, filename)
@@ -1411,6 +1413,20 @@ function srctext_files(f::IO, srctextpos::Int64)
14111413
return files
14121414
end
14131415

1416+
function get_preferences_hash(uuid::UUID, cache::TOMLCache = TOMLCache())
1417+
# check that project preferences match by first loading the Project.toml
1418+
active_project_file = Base.active_project()
1419+
if isfile(active_project_file)
1420+
preferences = get(parsed_toml(cache, active_project_file), "preferences", Dict{String,Any}())
1421+
if haskey(preferences, string(uuid))
1422+
return UInt64(hash(preferences[string(uuid)]))
1423+
end
1424+
end
1425+
return UInt64(hash(Dict{String,Any}()))
1426+
end
1427+
get_preferences_hash(::Nothing, cache::TOMLCache = TOMLCache()) = hash(Dict{String,Any}())
1428+
get_preferences_hash(m::Module, cache::TOMLCache = TOMLCache()) = get_preferences_hash(PkgId(m).uuid, cache)
1429+
14141430
# returns true if it "cachefile.ji" is stale relative to "modpath.jl"
14151431
# otherwise returns the list of dependencies to also check
14161432
stale_cachefile(modpath::String, cachefile::String) = stale_cachefile(modpath, cachefile, TOMLCache())
@@ -1421,7 +1437,7 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
14211437
@debug "Rejecting cache file $cachefile due to it containing an invalid cache header"
14221438
return true # invalid cache file
14231439
end
1424-
(modules, (includes, requires), required_modules) = parse_cache_header(io)
1440+
modules, (includes, requires), required_modules, srctextpos, prefs_hash = parse_cache_header(io)
14251441
id = isempty(modules) ? nothing : first(modules).first
14261442
modules = Dict{PkgId, UInt64}(modules)
14271443

@@ -1496,6 +1512,12 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
14961512
end
14971513

14981514
if isa(id, PkgId)
1515+
curr_prefs_hash = get_preferences_hash(id.uuid, cache)
1516+
if prefs_hash != curr_prefs_hash
1517+
@debug "Rejecting cache file $cachefile because preferences hash does not match 0x$(string(prefs_hash, base=16)) != 0x$(string(curr_prefs_hash, base=16))"
1518+
return true
1519+
end
1520+
14991521
pkgorigins[id] = PkgOrigin(cachefile)
15001522
end
15011523

base/sysimg.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ let
4747
:Distributed,
4848
:SharedArrays,
4949
:TOML,
50+
:Preferences,
5051
:Artifacts,
5152
:Pkg,
5253
:Test,

src/dump.c

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,31 @@ static int64_t write_dependency_list(ios_t *s, jl_array_t **udepsp, jl_array_t *
11231123
write_int32(s, 0);
11241124
}
11251125
write_int32(s, 0); // terminator, for ease of reading
1126+
1127+
// Calculate Preferences hash for current package.
1128+
jl_value_t *prefs_hash = NULL;
1129+
if (jl_base_module) {
1130+
// Toplevel module is the module we're currently compiling, use it to get our preferences hash
1131+
jl_value_t * toplevel = (jl_value_t*)jl_get_global(jl_base_module, jl_symbol("__toplevel__"));
1132+
jl_value_t * prefs_hash_func = jl_get_global(jl_base_module, jl_symbol("get_preferences_hash"));
1133+
1134+
if (toplevel && prefs_hash_func) {
1135+
// call get_preferences_hash(__toplevel__)
1136+
jl_value_t *prefs_hash_args[2] = {prefs_hash_func, (jl_value_t*)toplevel};
1137+
size_t last_age = jl_get_ptls_states()->world_age;
1138+
jl_get_ptls_states()->world_age = jl_world_counter;
1139+
prefs_hash = (jl_value_t*)jl_apply(prefs_hash_args, 2);
1140+
jl_get_ptls_states()->world_age = last_age;
1141+
}
1142+
}
1143+
1144+
// If we successfully got the preferences, write it out, otherwise write `0` for this `.ji` file.
1145+
if (prefs_hash != NULL) {
1146+
write_uint64(s, jl_unbox_uint64(prefs_hash));
1147+
} else {
1148+
write_uint64(s, 0);
1149+
}
1150+
11261151
// write a dummy file position to indicate the beginning of the source-text
11271152
pos = ios_pos(s);
11281153
ios_seek(s, initial_pos);

stdlib/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ $(build_datarootdir)/julia/stdlib/$(VERSDIR):
1616

1717
STDLIBS = Artifacts Base64 CRC32c Dates DelimitedFiles Distributed FileWatching \
1818
Future InteractiveUtils Libdl LibGit2 LinearAlgebra Logging \
19-
Markdown Mmap Printf Profile Random REPL Serialization SHA \
19+
Markdown Mmap Preferences Printf Profile Random REPL Serialization SHA \
2020
SharedArrays Sockets SparseArrays SuiteSparse Test TOML Unicode UUIDs
2121
STDLIBS_EXT = Pkg Statistics
2222
PKG_GIT_URL := git://github.com/JuliaLang/Pkg.jl.git

stdlib/Preferences/Project.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name = "Preferences"
2+
uuid = "21216c6a-2e73-6563-6e65-726566657250"
3+
4+
[deps]
5+
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
6+
7+
[extras]
8+
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
9+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
10+
11+
[targets]
12+
test = ["Test", "Pkg"]

stdlib/Preferences/docs/src/index.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Preferences
2+
3+
!!! compat "Julia 1.6"
4+
Julia's `Preferences` API requires at least Julia 1.6.
5+
6+
Preferences support embedding a simple `Dict` of metadata for a package on a per-project basis. These preferences allow for packages to set simple, persistent pieces of data that the user has selected, that can persist across multiple versions of a package.
7+
8+
## API Overview
9+
10+
`Preferences` are used primarily through the `@load_preferences`, `@save_preferences` and `@modify_preferences` macros. These macros will auto-detect the UUID of the calling package, throwing an error if the calling module does not belong to a package. The function forms can be used to load, save or modify preferences belonging to another package.
11+
12+
Example usage:
13+
14+
```julia
15+
using Preferences
16+
17+
function get_preferred_backend()
18+
prefs = @load_preferences()
19+
return get(prefs, "backend", "native")
20+
end
21+
22+
function set_backend(new_backend)
23+
@modify_preferences!() do prefs
24+
prefs["backend"] = new_backend
25+
end
26+
end
27+
```
28+
29+
By default, preferences are stored within the `Project.toml` file of the currently-active project, and as such all new projects will start from a blank state, with all preferences being un-set.
30+
Package authors that wish to have a default value set for their preferences should use the `get(prefs, key, default)` pattern as shown in the code example above.
31+
32+
# API Reference
33+
34+
!!! compat "Julia 1.6"
35+
Julia's `Preferences` API requires at least Julia 1.6.
36+
37+
```@docs
38+
Preferences.load_preferences
39+
Preferences.@load_preferences
40+
Preferences.save_preferences!
41+
Preferences.@save_preferences!
42+
Preferences.modify_preferences!
43+
Preferences.@modify_preferences!
44+
Preferences.clear_preferences!
45+
Preferences.@clear_preferences!
46+
```

stdlib/Preferences/src/Preferences.jl

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
module Preferences
2+
using TOML
3+
using Base: UUID
4+
5+
export load_preferences, @load_preferences,
6+
save_preferences!, @save_preferences!,
7+
modify_preferences!, @modify_preferences!,
8+
clear_preferences!, @clear_preferences!
9+
10+
# Helper function to get the UUID of a module, throwing an error if it can't.
11+
function get_uuid(m::Module)
12+
uuid = Base.PkgId(m).uuid
13+
if uuid === nothing
14+
throw(ArgumentError("Module does not correspond to a loaded package!"))
15+
end
16+
return uuid
17+
end
18+
19+
20+
"""
21+
load_preferences(uuid_or_module)
22+
23+
Load the preferences for the given package, returning them as a `Dict`. Most users
24+
should use the `@load_preferences()` macro which auto-determines the calling `Module`.
25+
"""
26+
function load_preferences(uuid::UUID)
27+
prefs = Dict{String,Any}()
28+
29+
# Finally, load from the currently-active project:
30+
proj_path = Base.active_project()
31+
if isfile(proj_path)
32+
project = TOML.parsefile(proj_path)
33+
if haskey(project, "preferences") && isa(project["preferences"], Dict)
34+
prefs = get(project["preferences"], string(uuid), Dict())
35+
end
36+
end
37+
return prefs
38+
end
39+
load_preferences(m::Module) = load_preferences(get_uuid(m))
40+
41+
42+
"""
43+
save_preferences!(uuid_or_module, prefs::Dict)
44+
45+
Save the preferences for the given package. Most users should use the
46+
`@save_preferences!()` macro which auto-determines the calling `Module`. See also the
47+
`modify_preferences!()` function (and the associated `@modifiy_preferences!()` macro) for
48+
easy load/modify/save workflows.
49+
"""
50+
function save_preferences!(uuid::UUID, prefs::Dict)
51+
# Save to Project.toml
52+
proj_path = Base.active_project()
53+
mkpath(dirname(proj_path))
54+
project = Dict{String,Any}()
55+
if isfile(proj_path)
56+
project = TOML.parsefile(proj_path)
57+
end
58+
if !haskey(project, "preferences")
59+
project["preferences"] = Dict{String,Any}()
60+
end
61+
if !isa(project["preferences"], Dict)
62+
error("$(proj_path) has conflicting `preferences` entry type: Not a Dict!")
63+
end
64+
project["preferences"][string(uuid)] = prefs
65+
open(proj_path, "w") do io
66+
TOML.print(io, project, sorted=true)
67+
end
68+
return nothing
69+
end
70+
function save_preferences!(m::Module, prefs::Dict)
71+
return save_preferences!(get_uuid(m), prefs)
72+
end
73+
74+
75+
"""
76+
modify_preferences!(f::Function, uuid::UUID)
77+
modify_preferences!(f::Function, m::Module)
78+
79+
Supports `do`-block modification of preferences. Loads the preferences, passes them to a
80+
user function, then writes the modified `Dict` back to the preferences file. Example:
81+
82+
```julia
83+
modify_preferences!(@__MODULE__) do prefs
84+
prefs["key"] = "value"
85+
end
86+
```
87+
88+
This function returns the full preferences object. Most users should use the
89+
`@modify_preferences!()` macro which auto-determines the calling `Module`.
90+
Note that this method does not support modifying depot-wide preferences; modifications
91+
always are saved to the active project.
92+
"""
93+
function modify_preferences!(f::Function, uuid::UUID)
94+
prefs = load_preferences(uuid)
95+
f(prefs)
96+
save_preferences!(uuid, prefs)
97+
return prefs
98+
end
99+
modify_preferences!(f::Function, m::Module) = modify_preferences!(f, get_uuid(m))
100+
101+
102+
"""
103+
clear_preferences!(uuid::UUID)
104+
clear_preferences!(m::Module)
105+
106+
Convenience method to remove all preferences for the given package. Most users should
107+
use the `@clear_preferences!()` macro, which auto-determines the calling `Module`.
108+
"""
109+
function clear_preferences!(uuid::UUID)
110+
# Clear the project preferences key, if it exists
111+
proj_path = Base.active_project()
112+
if isfile(proj_path)
113+
project = TOML.parsefile(proj_path)
114+
if haskey(project, "preferences") && isa(project["preferences"], Dict)
115+
delete!(project["preferences"], string(uuid))
116+
open(proj_path, "w") do io
117+
TOML.print(io, project, sorted=true)
118+
end
119+
end
120+
end
121+
end
122+
123+
124+
"""
125+
@load_preferences()
126+
127+
Convenience macro to call `load_preferences()` for the current package.
128+
"""
129+
macro load_preferences()
130+
return quote
131+
load_preferences($(esc(get_uuid(__module__))))
132+
end
133+
end
134+
135+
136+
"""
137+
@save_preferences!(prefs)
138+
139+
Convenience macro to call `save_preferences!()` for the current package.
140+
"""
141+
macro save_preferences!(prefs)
142+
return quote
143+
save_preferences!($(esc(get_uuid(__module__))), $(esc(prefs)))
144+
end
145+
end
146+
147+
148+
"""
149+
@modify_preferences!(func)
150+
151+
Convenience macro to call `modify_preferences!()` for the current package.
152+
"""
153+
macro modify_preferences!(func)
154+
return quote
155+
modify_preferences!($(esc(func)), $(esc(get_uuid(__module__))))
156+
end
157+
end
158+
159+
160+
"""
161+
@clear_preferences!()
162+
163+
Convenience macro to call `clear_preferences!()` for the current package.
164+
"""
165+
macro clear_preferences!()
166+
return quote
167+
preferences!($(esc(get_uuid(__module__))))
168+
end
169+
end
170+
171+
end # module Preferences
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name = "UsesPreferences"
2+
uuid = "056c4eb5-4491-6b91-3d28-8fffe3ee2af9"
3+
version = "0.1.0"
4+
5+
[deps]
6+
Preferences = "21216c6a-2e73-6563-6e65-726566657250"
7+
8+
[extras]
9+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
10+
11+
[targets]
12+
test = ["Test"]

0 commit comments

Comments
 (0)