Skip to content

Commit 9bc5740

Browse files
committed
Add CompilePreferences standard library
This commit adds the `CompilePreferences` 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 commid adds the `CompilePreferences` 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 608592c commit 9bc5740

File tree

14 files changed

+483
-16
lines changed

14 files changed

+483
-16
lines changed

NEWS.md

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

115117
#### LinearAlgebra
116118

base/loading.jl

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -943,7 +943,7 @@ function _require(pkg::PkgId, cache::TOMLCache)
943943
if (0 == ccall(:jl_generating_output, Cint, ())) || (JLOptions().incremental != 0)
944944
# spawn off a new incremental pre-compile task for recursive `require` calls
945945
# or if the require search declared it was pre-compiled before (and therefore is expected to still be pre-compilable)
946-
cachefile = compilecache(pkg, path)
946+
cachefile = compilecache(pkg, path, cache)
947947
if isa(cachefile, Exception)
948948
if precompilableerror(cachefile)
949949
verbosity = isinteractive() ? CoreLogging.Info : CoreLogging.Debug
@@ -1183,7 +1183,7 @@ end
11831183
@assert precompile(create_expr_cache, (String, String, typeof(_concrete_dependencies), Nothing))
11841184
@assert precompile(create_expr_cache, (String, String, typeof(_concrete_dependencies), UUID))
11851185

1186-
function compilecache_path(pkg::PkgId)::String
1186+
function compilecache_path(pkg::PkgId, cache::TOMLCache)::String
11871187
entrypath, entryfile = cache_file_entry(pkg)
11881188
cachepath = joinpath(DEPOT_PATH[1], entrypath)
11891189
isdir(cachepath) || mkpath(cachepath)
@@ -1193,6 +1193,7 @@ function compilecache_path(pkg::PkgId)::String
11931193
crc = _crc32c(something(Base.active_project(), ""))
11941194
crc = _crc32c(unsafe_string(JLOptions().image_file), crc)
11951195
crc = _crc32c(unsafe_string(JLOptions().julia_bin), crc)
1196+
crc = _crc32c(get_preferences_hash(pkg.uuid, cache), crc)
11961197
project_precompile_slug = slug(crc, 5)
11971198
abspath(cachepath, string(entryfile, "_", project_precompile_slug, ".ji"))
11981199
end
@@ -1209,14 +1210,14 @@ for important notes.
12091210
function compilecache(pkg::PkgId, cache::TOMLCache = TOMLCache())
12101211
path = locate_package(pkg, cache)
12111212
path === nothing && throw(ArgumentError("$pkg not found during precompilation"))
1212-
return compilecache(pkg, path)
1213+
return compilecache(pkg, path, cache)
12131214
end
12141215

12151216
const MAX_NUM_PRECOMPILE_FILES = 10
12161217

1217-
function compilecache(pkg::PkgId, path::String)
1218+
function compilecache(pkg::PkgId, path::String, cache::TOMLCache = TOMLCache())
12181219
# decide where to put the resulting cache file
1219-
cachefile = compilecache_path(pkg)
1220+
cachefile = compilecache_path(pkg, cache)
12201221
cachepath = dirname(cachefile)
12211222
# prune the directory with cache files
12221223
if pkg.uuid !== nothing
@@ -1320,6 +1321,8 @@ function parse_cache_header(f::IO)
13201321
end
13211322
totbytes -= 4 + 4 + n2 + 8
13221323
end
1324+
prefs_hash = read(f, UInt64)
1325+
totbytes -= 8
13231326
@assert totbytes == 12 "header of cache file appears to be corrupt"
13241327
srctextpos = read(f, Int64)
13251328
# read the list of modules that are required to be present during loading
@@ -1332,7 +1335,7 @@ function parse_cache_header(f::IO)
13321335
build_id = read(f, UInt64) # build id
13331336
push!(required_modules, PkgId(uuid, sym) => build_id)
13341337
end
1335-
return modules, (includes, requires), required_modules, srctextpos
1338+
return modules, (includes, requires), required_modules, srctextpos, prefs_hash
13361339
end
13371340

13381341
function parse_cache_header(cachefile::String; srcfiles_only::Bool=false)
@@ -1341,21 +1344,21 @@ function parse_cache_header(cachefile::String; srcfiles_only::Bool=false)
13411344
!isvalid_cache_header(io) && throw(ArgumentError("Invalid header in cache file $cachefile."))
13421345
ret = parse_cache_header(io)
13431346
srcfiles_only || return ret
1344-
modules, (includes, requires), required_modules, srctextpos = ret
1347+
modules, (includes, requires), required_modules, srctextpos, prefs_hash = ret
13451348
srcfiles = srctext_files(io, srctextpos)
13461349
delidx = Int[]
13471350
for (i, chi) in enumerate(includes)
13481351
chi.filename srcfiles || push!(delidx, i)
13491352
end
13501353
deleteat!(includes, delidx)
1351-
return modules, (includes, requires), required_modules, srctextpos
1354+
return modules, (includes, requires), required_modules, srctextpos, prefs_hash
13521355
finally
13531356
close(io)
13541357
end
13551358
end
13561359

13571360
function cache_dependencies(f::IO)
1358-
defs, (includes, requires), modules = parse_cache_header(f)
1361+
defs, (includes, requires), modules, srctextpos, prefs_hash = parse_cache_header(f)
13591362
return modules, map(chi -> (chi.filename, chi.mtime), includes) # return just filename and mtime
13601363
end
13611364

@@ -1370,7 +1373,7 @@ function cache_dependencies(cachefile::String)
13701373
end
13711374

13721375
function read_dependency_src(io::IO, filename::AbstractString)
1373-
modules, (includes, requires), required_modules, srctextpos = parse_cache_header(io)
1376+
modules, (includes, requires), required_modules, srctextpos, prefs_hash = parse_cache_header(io)
13741377
srctextpos == 0 && error("no source-text stored in cache file")
13751378
seek(io, srctextpos)
13761379
return _read_dependency_src(io, filename)
@@ -1415,6 +1418,20 @@ function srctext_files(f::IO, srctextpos::Int64)
14151418
return files
14161419
end
14171420

1421+
function get_preferences_hash(uuid::UUID, cache::TOMLCache = TOMLCache())
1422+
# check that project preferences match by first loading the Project.toml
1423+
active_project_file = Base.active_project()
1424+
if isfile(active_project_file)
1425+
preferences = get(parsed_toml(cache, active_project_file), "preferences", Dict{String,Any}())
1426+
if haskey(preferences, string(uuid))
1427+
return UInt64(hash(preferences[string(uuid)]))
1428+
end
1429+
end
1430+
return UInt64(hash(Dict{String,Any}()))
1431+
end
1432+
get_preferences_hash(::Nothing, cache::TOMLCache = TOMLCache()) = UInt64(hash(Dict{String,Any}()))
1433+
get_preferences_hash(m::Module, cache::TOMLCache = TOMLCache()) = get_preferences_hash(PkgId(m).uuid, cache)
1434+
14181435
# returns true if it "cachefile.ji" is stale relative to "modpath.jl"
14191436
# otherwise returns the list of dependencies to also check
14201437
stale_cachefile(modpath::String, cachefile::String) = stale_cachefile(modpath, cachefile, TOMLCache())
@@ -1425,7 +1442,7 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
14251442
@debug "Rejecting cache file $cachefile due to it containing an invalid cache header"
14261443
return true # invalid cache file
14271444
end
1428-
(modules, (includes, requires), required_modules) = parse_cache_header(io)
1445+
modules, (includes, requires), required_modules, srctextpos, prefs_hash = parse_cache_header(io)
14291446
id = isempty(modules) ? nothing : first(modules).first
14301447
modules = Dict{PkgId, UInt64}(modules)
14311448

@@ -1500,6 +1517,12 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
15001517
end
15011518

15021519
if isa(id, PkgId)
1520+
curr_prefs_hash = get_preferences_hash(id.uuid, cache)
1521+
if prefs_hash != curr_prefs_hash
1522+
@debug "Rejecting cache file $cachefile because preferences hash does not match 0x$(string(prefs_hash, base=16)) != 0x$(string(curr_prefs_hash, base=16))"
1523+
return true
1524+
end
1525+
15031526
get!(PkgOrigin, pkgorigins, id).cachepath = cachefile
15041527
end
15051528

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+
:CompilePreferences,
5051
:Artifacts,
5152
:Pkg,
5253
:Test,

base/util.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,8 @@ _crc32c(io::IO, crc::UInt32=0x00000000) = _crc32c(io, typemax(Int64), crc)
385385
_crc32c(io::IOStream, crc::UInt32=0x00000000) = _crc32c(io, filesize(io)-position(io), crc)
386386
_crc32c(uuid::UUID, crc::UInt32=0x00000000) =
387387
ccall(:jl_crc32c, UInt32, (UInt32, Ref{UInt128}, Csize_t), crc, uuid.value, 16)
388+
_crc32c(x::Integer, crc::UInt32=0x00000000) =
389+
ccall(:jl_crc32c, UInt32, (UInt32, Vector{UInt8}, Csize_t), crc, reinterpret(UInt8, [x]), sizeof(x))
388390

389391
"""
390392
@kwdef typedef

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 CompilePreferences 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);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name = "CompilePreferences"
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"]
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# CompilePreferences
2+
3+
!!! compat "Julia 1.6"
4+
Julia's `CompilePreferences` API requires at least Julia 1.6.
5+
6+
`CompilePreferences` 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+
`CompilePreferences` 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 CompilePreferences
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 `CompilePreferences` API requires at least Julia 1.6.
36+
37+
```@docs
38+
CompilePreferences.load_preferences
39+
CompilePreferences.@load_preferences
40+
CompilePreferences.save_preferences!
41+
CompilePreferences.@save_preferences!
42+
CompilePreferences.modify_preferences!
43+
CompilePreferences.@modify_preferences!
44+
CompilePreferences.clear_preferences!
45+
CompilePreferences.@clear_preferences!
46+
```

0 commit comments

Comments
 (0)