Skip to content

Commit f510352

Browse files
committed
[loading]: Rework preferences loading
Implements the `Preferences` loading framework as outlined in [0]. The most drastic change is that the list of compile-time preferences is no longer sequestered within its own dictionary, but is instead autodetected at compile-time and communicated back to the compiler. This list of compile-time preferences is now embedded as an array of strings that the loader must load, then index into the preferences dictionary with that list to check the preferences hash. In a somewhat bizarre turn of events, because we want the `.ji` filename to incorporate the preferences hash, and because we can't know how to generate the hash until after we've precompiled, I had to move the `.ji` filename generation step to _after_ we precompile the `.ji` file. [0]: #37791 (comment)
1 parent 2fe2b43 commit f510352

File tree

2 files changed

+207
-72
lines changed

2 files changed

+207
-72
lines changed

base/loading.jl

Lines changed: 181 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ end
273273

274274
const project_names = ("JuliaProject.toml", "Project.toml")
275275
const manifest_names = ("JuliaManifest.toml", "Manifest.toml")
276+
const preferences_names = ("JuliaPreferences.toml", "Preferences.toml")
277+
const local_preferences_names = ("LocalJuliaPreferences.toml", "LocalPreferences.toml")
276278

277279
# classify the LOAD_PATH entry to be one of:
278280
# - `false`: nonexistant / nothing to see here
@@ -322,31 +324,6 @@ function manifest_deps_get(env::String, where::PkgId, name::String, cache::TOMLC
322324
return nothing
323325
end
324326

325-
function uuid_in_environment(project_file::String, uuid::UUID, cache::TOMLCache)
326-
# First, check to see if we're looking for the environment itself
327-
proj_uuid = get(parsed_toml(cache, project_file), "uuid", nothing)
328-
if proj_uuid !== nothing && UUID(proj_uuid) == uuid
329-
return true
330-
end
331-
332-
# Check to see if there's a Manifest.toml associated with this project
333-
manifest_file = project_file_manifest_path(project_file, cache)
334-
if manifest_file === nothing
335-
return false
336-
end
337-
manifest = parsed_toml(cache, manifest_file)
338-
for (dep_name, entries) in manifest
339-
for entry in entries
340-
entry_uuid = get(entry, "uuid", nothing)::Union{String, Nothing}
341-
if uuid !== nothing && UUID(entry_uuid) == uuid
342-
return true
343-
end
344-
end
345-
end
346-
# If all else fails, return `false`
347-
return false
348-
end
349-
350327
function manifest_uuid_path(env::String, pkg::PkgId, cache::TOMLCache)::Union{Nothing,String}
351328
project_file = env_project_file(env)
352329
if project_file isa String
@@ -1220,7 +1197,12 @@ end
12201197
@assert precompile(create_expr_cache, (PkgId, String, String, typeof(_concrete_dependencies), Bool))
12211198
@assert precompile(create_expr_cache, (PkgId, String, String, typeof(_concrete_dependencies), Bool))
12221199

1223-
function compilecache_path(pkg::PkgId, cache::TOMLCache)::String
1200+
function compilecache_dir(pkg::PkgId)
1201+
entrypath, entryfile = cache_file_entry(pkg)
1202+
return joinpath(DEPOT_PATH[1], entrypath)
1203+
end
1204+
1205+
function compilecache_path(pkg::PkgId, prefs_hash::UInt64, cache::TOMLCache)::String
12241206
entrypath, entryfile = cache_file_entry(pkg)
12251207
cachepath = joinpath(DEPOT_PATH[1], entrypath)
12261208
isdir(cachepath) || mkpath(cachepath)
@@ -1230,7 +1212,7 @@ function compilecache_path(pkg::PkgId, cache::TOMLCache)::String
12301212
crc = _crc32c(something(Base.active_project(), ""))
12311213
crc = _crc32c(unsafe_string(JLOptions().image_file), crc)
12321214
crc = _crc32c(unsafe_string(JLOptions().julia_bin), crc)
1233-
crc = _crc32c(get_preferences_hash(pkg.uuid, cache), crc)
1215+
crc = _crc32c(prefs_hash, crc)
12341216
project_precompile_slug = slug(crc, 5)
12351217
abspath(cachepath, string(entryfile, "_", project_precompile_slug, ".ji"))
12361218
end
@@ -1254,17 +1236,8 @@ const MAX_NUM_PRECOMPILE_FILES = 10
12541236

12551237
function compilecache(pkg::PkgId, path::String, cache::TOMLCache = TOMLCache(), show_errors::Bool = true)
12561238
# decide where to put the resulting cache file
1257-
cachefile = compilecache_path(pkg, cache)
1258-
cachepath = dirname(cachefile)
1259-
# prune the directory with cache files
1260-
if pkg.uuid !== nothing
1261-
entrypath, entryfile = cache_file_entry(pkg)
1262-
cachefiles = filter!(x -> startswith(x, entryfile * "_"), readdir(cachepath))
1263-
if length(cachefiles) >= MAX_NUM_PRECOMPILE_FILES
1264-
idx = findmin(mtime.(joinpath.(cachepath, cachefiles)))[2]
1265-
rm(joinpath(cachepath, cachefiles[idx]))
1266-
end
1267-
end
1239+
cachepath = compilecache_dir(pkg)
1240+
12681241
# build up the list of modules that we want the precompile process to preserve
12691242
concrete_deps = copy(_concrete_dependencies)
12701243
for (key, mod) in loaded_modules
@@ -1278,6 +1251,7 @@ function compilecache(pkg::PkgId, path::String, cache::TOMLCache = TOMLCache(),
12781251

12791252
# create a temporary file in `cachepath` directory, write the cache in it,
12801253
# write the checksum, _and then_ atomically move the file to `cachefile`.
1254+
mkpath(cachepath)
12811255
tmppath, tmpio = mktemp(cachepath)
12821256
local p
12831257
try
@@ -1291,6 +1265,21 @@ function compilecache(pkg::PkgId, path::String, cache::TOMLCache = TOMLCache(),
12911265
# inherit permission from the source file
12921266
chmod(tmppath, filemode(path) & 0o777)
12931267

1268+
# Read preferences hash back from .ji file (we can't precompute because
1269+
# we don't actually know what the list of compile-time preferences are without compiling)
1270+
prefs_hash = preferences_hash(tmppath)
1271+
cachefile = compilecache_path(pkg, prefs_hash, cache)
1272+
1273+
# prune the directory with cache files
1274+
if pkg.uuid !== nothing
1275+
entrypath, entryfile = cache_file_entry(pkg)
1276+
cachefiles = filter!(x -> startswith(x, entryfile * "_"), readdir(cachepath))
1277+
if length(cachefiles) >= MAX_NUM_PRECOMPILE_FILES
1278+
idx = findmin(mtime.(joinpath.(cachepath, cachefiles)))[2]
1279+
rm(joinpath(cachepath, cachefiles[idx]))
1280+
end
1281+
end
1282+
12941283
# this is atomic according to POSIX:
12951284
rename(tmppath, cachefile; force=true)
12961285
return cachefile
@@ -1334,7 +1323,10 @@ function parse_cache_header(f::IO)
13341323
requires = Pair{PkgId, PkgId}[]
13351324
while true
13361325
n2 = read(f, Int32)
1337-
n2 == 0 && break
1326+
if n2 == 0
1327+
totbytes -= 4
1328+
break
1329+
end
13381330
depname = String(read(f, n2))
13391331
mtime = read(f, Float64)
13401332
n1 = read(f, Int32)
@@ -1358,10 +1350,20 @@ function parse_cache_header(f::IO)
13581350
end
13591351
totbytes -= 4 + 4 + n2 + 8
13601352
end
1353+
prefs = String[]
1354+
while true
1355+
n2 = read(f, Int32)
1356+
if n2 == 0
1357+
totbytes -= 4
1358+
break
1359+
end
1360+
push!(prefs, String(read(f, n2)))
1361+
totbytes -= 4 + n2
1362+
end
13611363
prefs_hash = read(f, UInt64)
1362-
totbytes -= 8
1363-
@assert totbytes == 12 "header of cache file appears to be corrupt"
13641364
srctextpos = read(f, Int64)
1365+
totbytes -= 8 + 8
1366+
@assert totbytes == 0 "header of cache file appears to be corrupt"
13651367
# read the list of modules that are required to be present during loading
13661368
required_modules = Vector{Pair{PkgId, UInt64}}()
13671369
while true
@@ -1372,7 +1374,7 @@ function parse_cache_header(f::IO)
13721374
build_id = read(f, UInt64) # build id
13731375
push!(required_modules, PkgId(uuid, sym) => build_id)
13741376
end
1375-
return modules, (includes, requires), required_modules, srctextpos, prefs_hash
1377+
return modules, (includes, requires), required_modules, srctextpos, prefs, prefs_hash
13761378
end
13771379

13781380
function parse_cache_header(cachefile::String; srcfiles_only::Bool=false)
@@ -1381,21 +1383,37 @@ function parse_cache_header(cachefile::String; srcfiles_only::Bool=false)
13811383
!isvalid_cache_header(io) && throw(ArgumentError("Invalid header in cache file $cachefile."))
13821384
ret = parse_cache_header(io)
13831385
srcfiles_only || return ret
1384-
modules, (includes, requires), required_modules, srctextpos, prefs_hash = ret
1386+
modules, (includes, requires), required_modules, srctextpos, prefs, prefs_hash = ret
13851387
srcfiles = srctext_files(io, srctextpos)
13861388
delidx = Int[]
13871389
for (i, chi) in enumerate(includes)
13881390
chi.filename srcfiles || push!(delidx, i)
13891391
end
13901392
deleteat!(includes, delidx)
1391-
return modules, (includes, requires), required_modules, srctextpos, prefs_hash
1393+
return modules, (includes, requires), required_modules, srctextpos, prefs, prefs_hash
1394+
finally
1395+
close(io)
1396+
end
1397+
end
1398+
1399+
1400+
1401+
preferences_hash(f::IO) = parse_cache_header(f)[end]
1402+
function preferences_hash(cachefile::String)
1403+
io = open(cachefile, "r")
1404+
try
1405+
if !isvalid_cache_header(io)
1406+
throw(ArgumentError("Invalid header in cache file $cachefile."))
1407+
end
1408+
return preferences_hash(io)
13921409
finally
13931410
close(io)
13941411
end
13951412
end
13961413

1414+
13971415
function cache_dependencies(f::IO)
1398-
defs, (includes, requires), modules, srctextpos, prefs_hash = parse_cache_header(f)
1416+
defs, (includes, requires), modules, srctextpos, prefs, prefs_hash = parse_cache_header(f)
13991417
return modules, map(chi -> (chi.filename, chi.mtime), includes) # return just filename and mtime
14001418
end
14011419

@@ -1410,7 +1428,7 @@ function cache_dependencies(cachefile::String)
14101428
end
14111429

14121430
function read_dependency_src(io::IO, filename::AbstractString)
1413-
modules, (includes, requires), required_modules, srctextpos, prefs_hash = parse_cache_header(io)
1431+
modules, (includes, requires), required_modules, srctextpos, prefs, prefs_hash = parse_cache_header(io)
14141432
srctextpos == 0 && error("no source-text stored in cache file")
14151433
seek(io, srctextpos)
14161434
return _read_dependency_src(io, filename)
@@ -1455,36 +1473,132 @@ function srctext_files(f::IO, srctextpos::Int64)
14551473
return files
14561474
end
14571475

1458-
# Find the Project.toml that we should load/store to for Preferences
1459-
function get_preferences_project_path(uuid::UUID, cache::TOMLCache = TOMLCache())
1476+
function get_uuid_name(project_toml::String, uuid::UUID, cache::TOMLCache = TOMLCache())
1477+
d = parsed_toml(cache, project_toml)
1478+
1479+
# Test to see if this UUID is mentioned in this `Project.toml`; either as
1480+
# the top-level UUID (e.g. that of the project itself) or as a dependency.
1481+
if haskey(d, "uuid") && haskey(d, "name") && UUID(d["uuid"]) == uuid
1482+
return d["name"]
1483+
elseif haskey(d, "deps")
1484+
# Search for the UUID as one of the deps
1485+
for (k, v) in d["deps"]
1486+
if v == uuid
1487+
return k
1488+
end
1489+
end
1490+
end
1491+
1492+
return nothing
1493+
end
1494+
1495+
function collect_preferences!(dicts::Vector{Dict}, project_toml::String,
1496+
pkg_name::String, cache::TOMLCache = TOMLCache())
1497+
# Look first for `Preferences.toml`, then for `LocalPreferences.toml`
1498+
for names_collection in (preferences_names, local_preferences_names)
1499+
for name in names_collection
1500+
toml_path = joinpath(project_dir, name)
1501+
if isfile(toml_path)
1502+
prefs = TOML.parsefile(toml_path)
1503+
push!(dicts, get(prefs, pkg_name, Dict()))
1504+
1505+
# If we find `JuliaPreferences.toml`, don't look for `Preferences.toml`,
1506+
# but do look for `Local(Julia)Preferences.toml`
1507+
break
1508+
end
1509+
end
1510+
end
1511+
1512+
return dicts
1513+
end
1514+
1515+
function get_clear_keys(d::Dict)
1516+
if haskey(d, "__clear_keys__") && isa(d["__clear_keys__"], Vector)
1517+
return d["__clear_keys__"]
1518+
end
1519+
return String[]
1520+
end
1521+
1522+
"""
1523+
recursive_merge(base::Dict, overrides::Dict...)
1524+
1525+
Helper function to merge preference dicts recursively, honoring overrides in nested
1526+
dictionaries properly.
1527+
"""
1528+
function recursive_merge(base::Dict, overrides::Dict...)
1529+
new_base = Base._typeddict(base, overrides...)
1530+
1531+
for override in overrides
1532+
# Clear keys are keys that should be deleted from any previous setting.
1533+
clear_keys = get_clear_keys(base)
1534+
for k in clear_keys
1535+
delete!(new_base, k)
1536+
end
1537+
1538+
for (k, v) in override
1539+
# Note that if `base` has a mapping that is _not_ a `Dict`, and `override`
1540+
if haskey(new_base, k) && isa(new_base[k], Dict) && isa(override[k], Dict)
1541+
new_base[k] = recursive_merge(new_base[k], override[k])
1542+
else
1543+
new_base[k] = override[k]
1544+
end
1545+
end
1546+
end
1547+
return new_base
1548+
end
1549+
1550+
function get_preferences(uuid::UUID, cache::TOMLCache = TOMLCache())
1551+
merged_prefs = Dict{String,Any}()
14601552
for env in load_path()
1461-
project_file = env_project_file(env)
1462-
if !isa(project_file, String)
1553+
project_toml = env_project_file(env)
1554+
if !isa(project_toml, String)
14631555
continue
14641556
end
1465-
if uuid_in_environment(project_file, uuid, cache)
1466-
return project_file
1557+
1558+
pkg_name = get_uuid_name(project_toml, uuid, cache)
1559+
if pkg_name !== nothing
1560+
project_dir = dirname(project_toml)
1561+
recursive_merge(merged_prefs, collect_preferences!(merged_prefs, project_dir, pkg_name, cache)...)
14671562
end
14681563
end
1469-
return nothing
1564+
return merged_prefs
14701565
end
14711566

1472-
function get_preferences(uuid::UUID, cache::TOMLCache = TOMLCache();
1473-
prefs_key::String = "compile-preferences")
1474-
project_path = get_preferences_project_path(uuid, cache)
1475-
if project_path !== nothing
1476-
preferences = get(parsed_toml(cache, project_path), prefs_key, Dict{String,Any}())
1477-
if haskey(preferences, string(uuid))
1478-
return preferences[string(uuid)]
1567+
function get_preferences_hash(uuid::UUID, prefs_list::Vector{String}, cache::TOMLCache = TOMLCache())
1568+
# Start from the "null" hash
1569+
h = get_preferences_hash(nothing, prefs_list, cache)
1570+
1571+
# Load the preferences
1572+
prefs = get_preferences(uuid, cache)
1573+
1574+
# Walk through each name that's called out as a compile-time preference
1575+
for name in prefs_list
1576+
if haskey(prefs, name)
1577+
h = hash(prefs[name], h)
14791578
end
14801579
end
1481-
# Fall back to default value of "no preferences".
1482-
return Dict{String,Any}()
1580+
return h
14831581
end
1484-
get_preferences_hash(uuid::UUID, cache::TOMLCache = TOMLCache()) = UInt64(hash(get_preferences(uuid, cache)))
1485-
get_preferences_hash(m::Module, cache::TOMLCache = TOMLCache()) = get_preferences_hash(PkgId(m).uuid, cache)
1486-
get_preferences_hash(::Nothing, cache::TOMLCache = TOMLCache()) = UInt64(hash(Dict{String,Any}()))
1582+
get_preferences_hash(m::Module, prefs_list::Vector{String}, cache::TOMLCache = TOMLCache()) = get_preferences_hash(PkgId(m).uuid, prefs_list, cache)
1583+
get_preferences_hash(::Nothing, prefs_list::Vector{String}, cache::TOMLCache = TOMLCache()) = UInt64(0x6e65726566657250)
14871584

1585+
# This is how we keep track of who is using what preferences at compile-time
1586+
const COMPILETIME_PREFERENCES = Dict{UUID,Set{String}}()
1587+
1588+
# In `Preferences.jl`, if someone calls `load_preference(@__MODULE__, key)` while we're precompiling,
1589+
# we mark that usage as a usage at compile-time and call this method, so that at the end of `.ji` generation,
1590+
# we can
1591+
function record_compiletime_preference(uuid::UUID, key::String)
1592+
if !haskey(COMPILETIME_PREFERENCES, uuid)
1593+
COMPILETIME_PREFERENCES[uuid] = Set((key,))
1594+
else
1595+
push!(COMPILETIME_PREFERENCES[uuid], key)
1596+
end
1597+
return nothing
1598+
end
1599+
get_compiletime_preferences(uuid::UUID) = get(COMPILETIME_PREFERENCES, uuid, String[])
1600+
get_compiletime_preferences(m::Module) = get_compiletime_preferences(PkgId(m).uuid)
1601+
get_compiletime_preferences(::Nothing) = String[]
14881602

14891603
# returns true if it "cachefile.ji" is stale relative to "modpath.jl"
14901604
# otherwise returns the list of dependencies to also check
@@ -1496,7 +1610,7 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
14961610
@debug "Rejecting cache file $cachefile due to it containing an invalid cache header"
14971611
return true # invalid cache file
14981612
end
1499-
modules, (includes, requires), required_modules, srctextpos, prefs_hash = parse_cache_header(io)
1613+
modules, (includes, requires), required_modules, srctextpos, prefs, prefs_hash = parse_cache_header(io)
15001614
id = isempty(modules) ? nothing : first(modules).first
15011615
modules = Dict{PkgId, UInt64}(modules)
15021616

@@ -1572,7 +1686,7 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
15721686
end
15731687

15741688
if isa(id, PkgId)
1575-
curr_prefs_hash = get_preferences_hash(id.uuid, cache)
1689+
curr_prefs_hash = get_preferences_hash(id.uuid, prefs, cache)
15761690
if prefs_hash != curr_prefs_hash
15771691
@debug "Rejecting cache file $cachefile because preferences hash does not match 0x$(string(prefs_hash, base=16)) != 0x$(string(curr_prefs_hash, base=16))"
15781692
return true

0 commit comments

Comments
 (0)