273
273
274
274
const project_names = (" JuliaProject.toml" , " Project.toml" )
275
275
const manifest_names = (" JuliaManifest.toml" , " Manifest.toml" )
276
+ const preferences_names = (" JuliaPreferences.toml" , " Preferences.toml" )
277
+ const local_preferences_names = (" LocalJuliaPreferences.toml" , " LocalPreferences.toml" )
276
278
277
279
# classify the LOAD_PATH entry to be one of:
278
280
# - `false`: nonexistant / nothing to see here
@@ -322,31 +324,6 @@ function manifest_deps_get(env::String, where::PkgId, name::String, cache::TOMLC
322
324
return nothing
323
325
end
324
326
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
-
350
327
function manifest_uuid_path (env:: String , pkg:: PkgId , cache:: TOMLCache ):: Union{Nothing,String}
351
328
project_file = env_project_file (env)
352
329
if project_file isa String
@@ -1220,7 +1197,12 @@ end
1220
1197
@assert precompile (create_expr_cache, (PkgId, String, String, typeof (_concrete_dependencies), Bool))
1221
1198
@assert precompile (create_expr_cache, (PkgId, String, String, typeof (_concrete_dependencies), Bool))
1222
1199
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
1224
1206
entrypath, entryfile = cache_file_entry (pkg)
1225
1207
cachepath = joinpath (DEPOT_PATH [1 ], entrypath)
1226
1208
isdir (cachepath) || mkpath (cachepath)
@@ -1230,7 +1212,7 @@ function compilecache_path(pkg::PkgId, cache::TOMLCache)::String
1230
1212
crc = _crc32c (something (Base. active_project (), " " ))
1231
1213
crc = _crc32c (unsafe_string (JLOptions (). image_file), crc)
1232
1214
crc = _crc32c (unsafe_string (JLOptions (). julia_bin), crc)
1233
- crc = _crc32c (get_preferences_hash (pkg . uuid, cache) , crc)
1215
+ crc = _crc32c (prefs_hash , crc)
1234
1216
project_precompile_slug = slug (crc, 5 )
1235
1217
abspath (cachepath, string (entryfile, " _" , project_precompile_slug, " .ji" ))
1236
1218
end
@@ -1254,17 +1236,8 @@ const MAX_NUM_PRECOMPILE_FILES = 10
1254
1236
1255
1237
function compilecache (pkg:: PkgId , path:: String , cache:: TOMLCache = TOMLCache (), show_errors:: Bool = true )
1256
1238
# 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
+
1268
1241
# build up the list of modules that we want the precompile process to preserve
1269
1242
concrete_deps = copy (_concrete_dependencies)
1270
1243
for (key, mod) in loaded_modules
@@ -1278,6 +1251,7 @@ function compilecache(pkg::PkgId, path::String, cache::TOMLCache = TOMLCache(),
1278
1251
1279
1252
# create a temporary file in `cachepath` directory, write the cache in it,
1280
1253
# write the checksum, _and then_ atomically move the file to `cachefile`.
1254
+ mkpath (cachepath)
1281
1255
tmppath, tmpio = mktemp (cachepath)
1282
1256
local p
1283
1257
try
@@ -1291,6 +1265,21 @@ function compilecache(pkg::PkgId, path::String, cache::TOMLCache = TOMLCache(),
1291
1265
# inherit permission from the source file
1292
1266
chmod (tmppath, filemode (path) & 0o777 )
1293
1267
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
+
1294
1283
# this is atomic according to POSIX:
1295
1284
rename (tmppath, cachefile; force= true )
1296
1285
return cachefile
@@ -1334,7 +1323,10 @@ function parse_cache_header(f::IO)
1334
1323
requires = Pair{PkgId, PkgId}[]
1335
1324
while true
1336
1325
n2 = read (f, Int32)
1337
- n2 == 0 && break
1326
+ if n2 == 0
1327
+ totbytes -= 4
1328
+ break
1329
+ end
1338
1330
depname = String (read (f, n2))
1339
1331
mtime = read (f, Float64)
1340
1332
n1 = read (f, Int32)
@@ -1358,10 +1350,20 @@ function parse_cache_header(f::IO)
1358
1350
end
1359
1351
totbytes -= 4 + 4 + n2 + 8
1360
1352
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
1361
1363
prefs_hash = read (f, UInt64)
1362
- totbytes -= 8
1363
- @assert totbytes == 12 " header of cache file appears to be corrupt"
1364
1364
srctextpos = read (f, Int64)
1365
+ totbytes -= 8 + 8
1366
+ @assert totbytes == 0 " header of cache file appears to be corrupt"
1365
1367
# read the list of modules that are required to be present during loading
1366
1368
required_modules = Vector {Pair{PkgId, UInt64}} ()
1367
1369
while true
@@ -1372,7 +1374,7 @@ function parse_cache_header(f::IO)
1372
1374
build_id = read (f, UInt64) # build id
1373
1375
push! (required_modules, PkgId (uuid, sym) => build_id)
1374
1376
end
1375
- return modules, (includes, requires), required_modules, srctextpos, prefs_hash
1377
+ return modules, (includes, requires), required_modules, srctextpos, prefs, prefs_hash
1376
1378
end
1377
1379
1378
1380
function parse_cache_header (cachefile:: String ; srcfiles_only:: Bool = false )
@@ -1381,21 +1383,37 @@ function parse_cache_header(cachefile::String; srcfiles_only::Bool=false)
1381
1383
! isvalid_cache_header (io) && throw (ArgumentError (" Invalid header in cache file $cachefile ." ))
1382
1384
ret = parse_cache_header (io)
1383
1385
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
1385
1387
srcfiles = srctext_files (io, srctextpos)
1386
1388
delidx = Int[]
1387
1389
for (i, chi) in enumerate (includes)
1388
1390
chi. filename ∈ srcfiles || push! (delidx, i)
1389
1391
end
1390
1392
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)
1392
1409
finally
1393
1410
close (io)
1394
1411
end
1395
1412
end
1396
1413
1414
+
1397
1415
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)
1399
1417
return modules, map (chi -> (chi. filename, chi. mtime), includes) # return just filename and mtime
1400
1418
end
1401
1419
@@ -1410,7 +1428,7 @@ function cache_dependencies(cachefile::String)
1410
1428
end
1411
1429
1412
1430
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)
1414
1432
srctextpos == 0 && error (" no source-text stored in cache file" )
1415
1433
seek (io, srctextpos)
1416
1434
return _read_dependency_src (io, filename)
@@ -1455,36 +1473,132 @@ function srctext_files(f::IO, srctextpos::Int64)
1455
1473
return files
1456
1474
end
1457
1475
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} ()
1460
1552
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)
1463
1555
continue
1464
1556
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)... )
1467
1562
end
1468
1563
end
1469
- return nothing
1564
+ return merged_prefs
1470
1565
end
1471
1566
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)
1479
1578
end
1480
1579
end
1481
- # Fall back to default value of "no preferences".
1482
- return Dict {String,Any} ()
1580
+ return h
1483
1581
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 )
1487
1584
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[]
1488
1602
1489
1603
# returns true if it "cachefile.ji" is stale relative to "modpath.jl"
1490
1604
# otherwise returns the list of dependencies to also check
@@ -1496,7 +1610,7 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
1496
1610
@debug " Rejecting cache file $cachefile due to it containing an invalid cache header"
1497
1611
return true # invalid cache file
1498
1612
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)
1500
1614
id = isempty (modules) ? nothing : first (modules). first
1501
1615
modules = Dict {PkgId, UInt64} (modules)
1502
1616
@@ -1572,7 +1686,7 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
1572
1686
end
1573
1687
1574
1688
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)
1576
1690
if prefs_hash != curr_prefs_hash
1577
1691
@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
1692
return true
0 commit comments