Skip to content

Commit e9e4d60

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 9b81a8a commit e9e4d60

File tree

15 files changed

+573
-19
lines changed

15 files changed

+573
-19
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: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,31 @@ function manifest_deps_get(env::String, where::PkgId, name::String, cache::TOMLC
321321
return nothing
322322
end
323323

324+
function uuid_in_environment(project_file::String, uuid::UUID, cache::TOMLCache)
325+
# First, check to see if we're looking for the environment itself
326+
proj_uuid = get(parsed_toml(cache, project_file), "uuid", nothing)
327+
if proj_uuid !== nothing && UUID(proj_uuid) == uuid
328+
return true
329+
end
330+
331+
# Check to see if there's a Manifest.toml associated with this project
332+
manifest_file = project_file_manifest_path(project_file, cache)
333+
if manifest_file === nothing
334+
return false
335+
end
336+
manifest = parsed_toml(cache, manifest_file)
337+
for (dep_name, entries) in manifest
338+
for entry in entries
339+
entry_uuid = get(entry, "uuid", nothing)::Union{String, Nothing}
340+
if uuid !== nothing && UUID(entry_uuid) == uuid
341+
return true
342+
end
343+
end
344+
end
345+
# If all else fails, return `false`
346+
return false
347+
end
348+
324349
function manifest_uuid_path(env::String, pkg::PkgId, cache::TOMLCache)::Union{Nothing,String}
325350
project_file = env_project_file(env)
326351
if project_file isa String
@@ -943,7 +968,7 @@ function _require(pkg::PkgId, cache::TOMLCache)
943968
if (0 == ccall(:jl_generating_output, Cint, ())) || (JLOptions().incremental != 0)
944969
# spawn off a new incremental pre-compile task for recursive `require` calls
945970
# 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)
971+
cachefile = compilecache(pkg, path, cache)
947972
if isa(cachefile, Exception)
948973
if precompilableerror(cachefile)
949974
verbosity = isinteractive() ? CoreLogging.Info : CoreLogging.Debug
@@ -1183,7 +1208,7 @@ end
11831208
@assert precompile(create_expr_cache, (String, String, typeof(_concrete_dependencies), Nothing))
11841209
@assert precompile(create_expr_cache, (String, String, typeof(_concrete_dependencies), UUID))
11851210

1186-
function compilecache_path(pkg::PkgId)::String
1211+
function compilecache_path(pkg::PkgId, cache::TOMLCache)::String
11871212
entrypath, entryfile = cache_file_entry(pkg)
11881213
cachepath = joinpath(DEPOT_PATH[1], entrypath)
11891214
isdir(cachepath) || mkpath(cachepath)
@@ -1193,6 +1218,7 @@ function compilecache_path(pkg::PkgId)::String
11931218
crc = _crc32c(something(Base.active_project(), ""))
11941219
crc = _crc32c(unsafe_string(JLOptions().image_file), crc)
11951220
crc = _crc32c(unsafe_string(JLOptions().julia_bin), crc)
1221+
crc = _crc32c(get_preferences_hash(pkg.uuid, cache), crc)
11961222
project_precompile_slug = slug(crc, 5)
11971223
abspath(cachepath, string(entryfile, "_", project_precompile_slug, ".ji"))
11981224
end
@@ -1209,14 +1235,14 @@ for important notes.
12091235
function compilecache(pkg::PkgId, cache::TOMLCache = TOMLCache())
12101236
path = locate_package(pkg, cache)
12111237
path === nothing && throw(ArgumentError("$pkg not found during precompilation"))
1212-
return compilecache(pkg, path)
1238+
return compilecache(pkg, path, cache)
12131239
end
12141240

12151241
const MAX_NUM_PRECOMPILE_FILES = 10
12161242

1217-
function compilecache(pkg::PkgId, path::String)
1243+
function compilecache(pkg::PkgId, path::String, cache::TOMLCache = TOMLCache())
12181244
# decide where to put the resulting cache file
1219-
cachefile = compilecache_path(pkg)
1245+
cachefile = compilecache_path(pkg, cache)
12201246
cachepath = dirname(cachefile)
12211247
# prune the directory with cache files
12221248
if pkg.uuid !== nothing
@@ -1320,6 +1346,8 @@ function parse_cache_header(f::IO)
13201346
end
13211347
totbytes -= 4 + 4 + n2 + 8
13221348
end
1349+
prefs_hash = read(f, UInt64)
1350+
totbytes -= 8
13231351
@assert totbytes == 12 "header of cache file appears to be corrupt"
13241352
srctextpos = read(f, Int64)
13251353
# read the list of modules that are required to be present during loading
@@ -1332,7 +1360,7 @@ function parse_cache_header(f::IO)
13321360
build_id = read(f, UInt64) # build id
13331361
push!(required_modules, PkgId(uuid, sym) => build_id)
13341362
end
1335-
return modules, (includes, requires), required_modules, srctextpos
1363+
return modules, (includes, requires), required_modules, srctextpos, prefs_hash
13361364
end
13371365

13381366
function parse_cache_header(cachefile::String; srcfiles_only::Bool=false)
@@ -1341,21 +1369,21 @@ function parse_cache_header(cachefile::String; srcfiles_only::Bool=false)
13411369
!isvalid_cache_header(io) && throw(ArgumentError("Invalid header in cache file $cachefile."))
13421370
ret = parse_cache_header(io)
13431371
srcfiles_only || return ret
1344-
modules, (includes, requires), required_modules, srctextpos = ret
1372+
modules, (includes, requires), required_modules, srctextpos, prefs_hash = ret
13451373
srcfiles = srctext_files(io, srctextpos)
13461374
delidx = Int[]
13471375
for (i, chi) in enumerate(includes)
13481376
chi.filename srcfiles || push!(delidx, i)
13491377
end
13501378
deleteat!(includes, delidx)
1351-
return modules, (includes, requires), required_modules, srctextpos
1379+
return modules, (includes, requires), required_modules, srctextpos, prefs_hash
13521380
finally
13531381
close(io)
13541382
end
13551383
end
13561384

13571385
function cache_dependencies(f::IO)
1358-
defs, (includes, requires), modules = parse_cache_header(f)
1386+
defs, (includes, requires), modules, srctextpos, prefs_hash = parse_cache_header(f)
13591387
return modules, map(chi -> (chi.filename, chi.mtime), includes) # return just filename and mtime
13601388
end
13611389

@@ -1370,7 +1398,7 @@ function cache_dependencies(cachefile::String)
13701398
end
13711399

13721400
function read_dependency_src(io::IO, filename::AbstractString)
1373-
modules, (includes, requires), required_modules, srctextpos = parse_cache_header(io)
1401+
modules, (includes, requires), required_modules, srctextpos, prefs_hash = parse_cache_header(io)
13741402
srctextpos == 0 && error("no source-text stored in cache file")
13751403
seek(io, srctextpos)
13761404
return _read_dependency_src(io, filename)
@@ -1415,6 +1443,37 @@ function srctext_files(f::IO, srctextpos::Int64)
14151443
return files
14161444
end
14171445

1446+
# Find the Project.toml that we should load/store to for Preferences
1447+
function get_preferences_project_path(uuid::UUID, cache::TOMLCache = TOMLCache())
1448+
for env in load_path()
1449+
project_file = env_project_file(env)
1450+
if !isa(project_file, String)
1451+
continue
1452+
end
1453+
if uuid_in_environment(project_file, uuid, cache)
1454+
return project_file
1455+
end
1456+
end
1457+
return nothing
1458+
end
1459+
1460+
function get_preferences(uuid::UUID, cache::TOMLCache = TOMLCache();
1461+
prefs_key::String = "compile-preferences")
1462+
project_path = get_preferences_project_path(uuid, cache)
1463+
if project_path !== nothing
1464+
preferences = get(parsed_toml(cache, project_path), prefs_key, Dict{String,Any}())
1465+
if haskey(preferences, string(uuid))
1466+
return preferences[string(uuid)]
1467+
end
1468+
end
1469+
# Fall back to default value of "no preferences".
1470+
return Dict{String,Any}()
1471+
end
1472+
get_preferences_hash(uuid::UUID, cache::TOMLCache = TOMLCache()) = UInt64(hash(get_preferences(uuid, cache)))
1473+
get_preferences_hash(m::Module, cache::TOMLCache = TOMLCache()) = get_preferences_hash(PkgId(m).uuid, cache)
1474+
get_preferences_hash(::Nothing, cache::TOMLCache = TOMLCache()) = UInt64(hash(Dict{String,Any}()))
1475+
1476+
14181477
# returns true if it "cachefile.ji" is stale relative to "modpath.jl"
14191478
# otherwise returns the list of dependencies to also check
14201479
stale_cachefile(modpath::String, cachefile::String) = stale_cachefile(modpath, cachefile, TOMLCache())
@@ -1425,7 +1484,7 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
14251484
@debug "Rejecting cache file $cachefile due to it containing an invalid cache header"
14261485
return true # invalid cache file
14271486
end
1428-
(modules, (includes, requires), required_modules) = parse_cache_header(io)
1487+
modules, (includes, requires), required_modules, srctextpos, prefs_hash = parse_cache_header(io)
14291488
id = isempty(modules) ? nothing : first(modules).first
14301489
modules = Dict{PkgId, UInt64}(modules)
14311490

@@ -1501,6 +1560,12 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
15011560
end
15021561

15031562
if isa(id, PkgId)
1563+
curr_prefs_hash = get_preferences_hash(id.uuid, cache)
1564+
if prefs_hash != curr_prefs_hash
1565+
@debug "Rejecting cache file $cachefile because preferences hash does not match 0x$(string(prefs_hash, base=16)) != 0x$(string(curr_prefs_hash, base=16))"
1566+
return true
1567+
end
1568+
15041569
get!(PkgOrigin, pkgorigins, id).cachepath = cachefile
15051570
end
15061571

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: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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, and trigger recompilation of the package when the preferences change, to allow for customization of package behavior at compile-time.
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+
Preferences are stored within the first `Project.toml` that represents an environment that contains the given UUID, even as a transitive dependency.
30+
If no project that contains the given UUID is found, the preference is recorded in the `Project.toml` file of the currently-active project.
31+
The initial state for preferences is an empty dictionary, 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.
32+
33+
# API Reference
34+
35+
!!! compat "Julia 1.6"
36+
Julia's `CompilePreferences` API requires at least Julia 1.6.
37+
38+
```@docs
39+
CompilePreferences.load_preferences
40+
CompilePreferences.@load_preferences
41+
CompilePreferences.save_preferences!
42+
CompilePreferences.@save_preferences!
43+
CompilePreferences.modify_preferences!
44+
CompilePreferences.@modify_preferences!
45+
CompilePreferences.clear_preferences!
46+
CompilePreferences.@clear_preferences!
47+
```

0 commit comments

Comments
 (0)