Skip to content

Commit b4e4324

Browse files
committed
[loading]: Add serialization/checking of Preferences.jl hash
This adds the calculation, serialization and verification of preferences hashes at code loading time. Preferences, as stored by the forthcoming `Preferences.jl` package within a top-level `Project.toml` file, are parsed by the `dump.c` and `loading.jl` code loading machinery and used to provide a compile-time preferences machinery.
1 parent 91d7e18 commit b4e4324

File tree

4 files changed

+105
-14
lines changed

4 files changed

+105
-14
lines changed

base/loading.jl

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,31 @@ function manifest_deps_get(env::String, where::PkgId, name::String, cache::TOMLC
322322
return nothing
323323
end
324324

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+
325350
function manifest_uuid_path(env::String, pkg::PkgId, cache::TOMLCache)::Union{Nothing,String}
326351
project_file = env_project_file(env)
327352
if project_file isa String
@@ -950,7 +975,7 @@ function _require(pkg::PkgId, cache::TOMLCache)
950975
if (0 == ccall(:jl_generating_output, Cint, ())) || (JLOptions().incremental != 0)
951976
# spawn off a new incremental pre-compile task for recursive `require` calls
952977
# or if the require search declared it was pre-compiled before (and therefore is expected to still be pre-compilable)
953-
cachefile = compilecache(pkg, path)
978+
cachefile = compilecache(pkg, path, cache)
954979
if isa(cachefile, Exception)
955980
if precompilableerror(cachefile)
956981
verbosity = isinteractive() ? CoreLogging.Info : CoreLogging.Debug
@@ -1195,7 +1220,7 @@ end
11951220
@assert precompile(create_expr_cache, (PkgId, String, String, typeof(_concrete_dependencies), Bool))
11961221
@assert precompile(create_expr_cache, (PkgId, String, String, typeof(_concrete_dependencies), Bool))
11971222

1198-
function compilecache_path(pkg::PkgId)::String
1223+
function compilecache_path(pkg::PkgId, cache::TOMLCache)::String
11991224
entrypath, entryfile = cache_file_entry(pkg)
12001225
cachepath = joinpath(DEPOT_PATH[1], entrypath)
12011226
isdir(cachepath) || mkpath(cachepath)
@@ -1205,6 +1230,7 @@ function compilecache_path(pkg::PkgId)::String
12051230
crc = _crc32c(something(Base.active_project(), ""))
12061231
crc = _crc32c(unsafe_string(JLOptions().image_file), crc)
12071232
crc = _crc32c(unsafe_string(JLOptions().julia_bin), crc)
1233+
crc = _crc32c(get_preferences_hash(pkg.uuid, cache), crc)
12081234
project_precompile_slug = slug(crc, 5)
12091235
abspath(cachepath, string(entryfile, "_", project_precompile_slug, ".ji"))
12101236
end
@@ -1218,18 +1244,17 @@ This can be used to reduce package load times. Cache files are stored in
12181244
`DEPOT_PATH[1]/compiled`. See [Module initialization and precompilation](@ref)
12191245
for important notes.
12201246
"""
1221-
function compilecache(pkg::PkgId, cache::TOMLCache = TOMLCache())
1247+
function compilecache(pkg::PkgId, cache::TOMLCache = TOMLCache(), show_errors::Bool = true)
12221248
path = locate_package(pkg, cache)
12231249
path === nothing && throw(ArgumentError("$pkg not found during precompilation"))
1224-
return compilecache(pkg, path)
1250+
return compilecache(pkg, path, cache, show_errors)
12251251
end
12261252

12271253
const MAX_NUM_PRECOMPILE_FILES = 10
12281254

1229-
# `show_errors` is an "internal" interface for Pkg.precompile
1230-
function compilecache(pkg::PkgId, path::String, show_errors::Bool = true)
1255+
function compilecache(pkg::PkgId, path::String, cache::TOMLCache = TOMLCache(), show_errors::Bool = true)
12311256
# decide where to put the resulting cache file
1232-
cachefile = compilecache_path(pkg)
1257+
cachefile = compilecache_path(pkg, cache)
12331258
cachepath = dirname(cachefile)
12341259
# prune the directory with cache files
12351260
if pkg.uuid !== nothing
@@ -1333,6 +1358,8 @@ function parse_cache_header(f::IO)
13331358
end
13341359
totbytes -= 4 + 4 + n2 + 8
13351360
end
1361+
prefs_hash = read(f, UInt64)
1362+
totbytes -= 8
13361363
@assert totbytes == 12 "header of cache file appears to be corrupt"
13371364
srctextpos = read(f, Int64)
13381365
# read the list of modules that are required to be present during loading
@@ -1345,7 +1372,7 @@ function parse_cache_header(f::IO)
13451372
build_id = read(f, UInt64) # build id
13461373
push!(required_modules, PkgId(uuid, sym) => build_id)
13471374
end
1348-
return modules, (includes, requires), required_modules, srctextpos
1375+
return modules, (includes, requires), required_modules, srctextpos, prefs_hash
13491376
end
13501377

13511378
function parse_cache_header(cachefile::String; srcfiles_only::Bool=false)
@@ -1354,21 +1381,21 @@ function parse_cache_header(cachefile::String; srcfiles_only::Bool=false)
13541381
!isvalid_cache_header(io) && throw(ArgumentError("Invalid header in cache file $cachefile."))
13551382
ret = parse_cache_header(io)
13561383
srcfiles_only || return ret
1357-
modules, (includes, requires), required_modules, srctextpos = ret
1384+
modules, (includes, requires), required_modules, srctextpos, prefs_hash = ret
13581385
srcfiles = srctext_files(io, srctextpos)
13591386
delidx = Int[]
13601387
for (i, chi) in enumerate(includes)
13611388
chi.filename srcfiles || push!(delidx, i)
13621389
end
13631390
deleteat!(includes, delidx)
1364-
return modules, (includes, requires), required_modules, srctextpos
1391+
return modules, (includes, requires), required_modules, srctextpos, prefs_hash
13651392
finally
13661393
close(io)
13671394
end
13681395
end
13691396

13701397
function cache_dependencies(f::IO)
1371-
defs, (includes, requires), modules = parse_cache_header(f)
1398+
defs, (includes, requires), modules, srctextpos, prefs_hash = parse_cache_header(f)
13721399
return modules, map(chi -> (chi.filename, chi.mtime), includes) # return just filename and mtime
13731400
end
13741401

@@ -1383,7 +1410,7 @@ function cache_dependencies(cachefile::String)
13831410
end
13841411

13851412
function read_dependency_src(io::IO, filename::AbstractString)
1386-
modules, (includes, requires), required_modules, srctextpos = parse_cache_header(io)
1413+
modules, (includes, requires), required_modules, srctextpos, prefs_hash = parse_cache_header(io)
13871414
srctextpos == 0 && error("no source-text stored in cache file")
13881415
seek(io, srctextpos)
13891416
return _read_dependency_src(io, filename)
@@ -1428,6 +1455,37 @@ function srctext_files(f::IO, srctextpos::Int64)
14281455
return files
14291456
end
14301457

1458+
# Find the Project.toml that we should load/store to for Preferences
1459+
function get_preferences_project_path(uuid::UUID, cache::TOMLCache = TOMLCache())
1460+
for env in load_path()
1461+
project_file = env_project_file(env)
1462+
if !isa(project_file, String)
1463+
continue
1464+
end
1465+
if uuid_in_environment(project_file, uuid, cache)
1466+
return project_file
1467+
end
1468+
end
1469+
return nothing
1470+
end
1471+
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)]
1479+
end
1480+
end
1481+
# Fall back to default value of "no preferences".
1482+
return Dict{String,Any}()
1483+
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}()))
1487+
1488+
14311489
# returns true if it "cachefile.ji" is stale relative to "modpath.jl"
14321490
# otherwise returns the list of dependencies to also check
14331491
stale_cachefile(modpath::String, cachefile::String) = stale_cachefile(modpath, cachefile, TOMLCache())
@@ -1438,7 +1496,7 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
14381496
@debug "Rejecting cache file $cachefile due to it containing an invalid cache header"
14391497
return true # invalid cache file
14401498
end
1441-
(modules, (includes, requires), required_modules) = parse_cache_header(io)
1499+
modules, (includes, requires), required_modules, srctextpos, prefs_hash = parse_cache_header(io)
14421500
id = isempty(modules) ? nothing : first(modules).first
14431501
modules = Dict{PkgId, UInt64}(modules)
14441502

@@ -1514,6 +1572,12 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
15141572
end
15151573

15161574
if isa(id, PkgId)
1575+
curr_prefs_hash = get_preferences_hash(id.uuid, cache)
1576+
if prefs_hash != curr_prefs_hash
1577+
@debug "Rejecting cache file $cachefile because preferences hash does not match 0x$(string(prefs_hash, base=16)) != 0x$(string(curr_prefs_hash, base=16))"
1578+
return true
1579+
end
1580+
15171581
get!(PkgOrigin, pkgorigins, id).cachepath = cachefile
15181582
end
15191583

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 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);

test/precompile.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ try
416416
""")
417417

418418
cachefile = Base.compilecache(Base.PkgId("FooBar"))
419-
@test cachefile == Base.compilecache_path(Base.PkgId("FooBar"))
419+
@test cachefile == Base.compilecache_path(Base.PkgId("FooBar"), Base.TOMLCache())
420420
@test isfile(joinpath(cachedir, "FooBar.ji"))
421421
@test Base.stale_cachefile(FooBar_file, joinpath(cachedir, "FooBar.ji")) isa Vector
422422
@test !isdefined(Main, :FooBar)

0 commit comments

Comments
 (0)