Skip to content

Commit

Permalink
Add CompilePreferences standard library
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
staticfloat committed Sep 16, 2020
1 parent b62ef14 commit b0f5abe
Show file tree
Hide file tree
Showing 14 changed files with 483 additions and 16 deletions.
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ Standard library changes
* The `Pkg.Artifacts` module has been imported as a separate standard library. It is still available as
`Pkg.Artifacts`, however starting from Julia v1.6+, packages may import simply `Artifacts` without importing
all of `Pkg` alongside. ([#37320])
* A new standard library, `CompilePreferences`, has been added to allow packages to store settings within the top-
level `Project.toml`, and force recompilation when the preferences are changed. ([#xxxxx])

#### LinearAlgebra

Expand Down
45 changes: 34 additions & 11 deletions base/loading.jl
Original file line number Diff line number Diff line change
Expand Up @@ -943,7 +943,7 @@ function _require(pkg::PkgId, cache::TOMLCache)
if (0 == ccall(:jl_generating_output, Cint, ())) || (JLOptions().incremental != 0)
# spawn off a new incremental pre-compile task for recursive `require` calls
# or if the require search declared it was pre-compiled before (and therefore is expected to still be pre-compilable)
cachefile = compilecache(pkg, path)
cachefile = compilecache(pkg, path, cache)
if isa(cachefile, Exception)
if precompilableerror(cachefile)
verbosity = isinteractive() ? CoreLogging.Info : CoreLogging.Debug
Expand Down Expand Up @@ -1183,7 +1183,7 @@ end
@assert precompile(create_expr_cache, (String, String, typeof(_concrete_dependencies), Nothing))
@assert precompile(create_expr_cache, (String, String, typeof(_concrete_dependencies), UUID))

function compilecache_path(pkg::PkgId)::String
function compilecache_path(pkg::PkgId, cache::TOMLCache)::String
entrypath, entryfile = cache_file_entry(pkg)
cachepath = joinpath(DEPOT_PATH[1], entrypath)
isdir(cachepath) || mkpath(cachepath)
Expand All @@ -1193,6 +1193,7 @@ function compilecache_path(pkg::PkgId)::String
crc = _crc32c(something(Base.active_project(), ""))
crc = _crc32c(unsafe_string(JLOptions().image_file), crc)
crc = _crc32c(unsafe_string(JLOptions().julia_bin), crc)
crc = _crc32c(get_preferences_hash(pkg.uuid, cache), crc)
project_precompile_slug = slug(crc, 5)
abspath(cachepath, string(entryfile, "_", project_precompile_slug, ".ji"))
end
Expand All @@ -1209,14 +1210,14 @@ for important notes.
function compilecache(pkg::PkgId, cache::TOMLCache = TOMLCache())
path = locate_package(pkg, cache)
path === nothing && throw(ArgumentError("$pkg not found during precompilation"))
return compilecache(pkg, path)
return compilecache(pkg, path, cache)
end

const MAX_NUM_PRECOMPILE_FILES = 10

function compilecache(pkg::PkgId, path::String)
function compilecache(pkg::PkgId, path::String, cache::TOMLCache = TOMLCache())
# decide where to put the resulting cache file
cachefile = compilecache_path(pkg)
cachefile = compilecache_path(pkg, cache)
cachepath = dirname(cachefile)
# prune the directory with cache files
if pkg.uuid !== nothing
Expand Down Expand Up @@ -1320,6 +1321,8 @@ function parse_cache_header(f::IO)
end
totbytes -= 4 + 4 + n2 + 8
end
prefs_hash = read(f, UInt64)
totbytes -= 8
@assert totbytes == 12 "header of cache file appears to be corrupt"
srctextpos = read(f, Int64)
# read the list of modules that are required to be present during loading
Expand All @@ -1332,7 +1335,7 @@ function parse_cache_header(f::IO)
build_id = read(f, UInt64) # build id
push!(required_modules, PkgId(uuid, sym) => build_id)
end
return modules, (includes, requires), required_modules, srctextpos
return modules, (includes, requires), required_modules, srctextpos, prefs_hash
end

function parse_cache_header(cachefile::String; srcfiles_only::Bool=false)
Expand All @@ -1341,21 +1344,21 @@ function parse_cache_header(cachefile::String; srcfiles_only::Bool=false)
!isvalid_cache_header(io) && throw(ArgumentError("Invalid header in cache file $cachefile."))
ret = parse_cache_header(io)
srcfiles_only || return ret
modules, (includes, requires), required_modules, srctextpos = ret
modules, (includes, requires), required_modules, srctextpos, prefs_hash = ret
srcfiles = srctext_files(io, srctextpos)
delidx = Int[]
for (i, chi) in enumerate(includes)
chi.filename srcfiles || push!(delidx, i)
end
deleteat!(includes, delidx)
return modules, (includes, requires), required_modules, srctextpos
return modules, (includes, requires), required_modules, srctextpos, prefs_hash
finally
close(io)
end
end

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

Expand All @@ -1370,7 +1373,7 @@ function cache_dependencies(cachefile::String)
end

function read_dependency_src(io::IO, filename::AbstractString)
modules, (includes, requires), required_modules, srctextpos = parse_cache_header(io)
modules, (includes, requires), required_modules, srctextpos, prefs_hash = parse_cache_header(io)
srctextpos == 0 && error("no source-text stored in cache file")
seek(io, srctextpos)
return _read_dependency_src(io, filename)
Expand Down Expand Up @@ -1415,6 +1418,20 @@ function srctext_files(f::IO, srctextpos::Int64)
return files
end

function get_preferences_hash(uuid::UUID, cache::TOMLCache = TOMLCache())
# check that project preferences match by first loading the Project.toml
active_project_file = Base.active_project()
if isfile(active_project_file)
preferences = get(parsed_toml(cache, active_project_file), "compile-preferences", Dict{String,Any}())
if haskey(preferences, string(uuid))
return UInt64(hash(preferences[string(uuid)]))
end
end
return UInt64(hash(Dict{String,Any}()))
end
get_preferences_hash(::Nothing, cache::TOMLCache = TOMLCache()) = UInt64(hash(Dict{String,Any}()))
get_preferences_hash(m::Module, cache::TOMLCache = TOMLCache()) = get_preferences_hash(PkgId(m).uuid, cache)

# returns true if it "cachefile.ji" is stale relative to "modpath.jl"
# otherwise returns the list of dependencies to also check
stale_cachefile(modpath::String, cachefile::String) = stale_cachefile(modpath, cachefile, TOMLCache())
Expand All @@ -1425,7 +1442,7 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
@debug "Rejecting cache file $cachefile due to it containing an invalid cache header"
return true # invalid cache file
end
(modules, (includes, requires), required_modules) = parse_cache_header(io)
modules, (includes, requires), required_modules, srctextpos, prefs_hash = parse_cache_header(io)
id = isempty(modules) ? nothing : first(modules).first
modules = Dict{PkgId, UInt64}(modules)

Expand Down Expand Up @@ -1500,6 +1517,12 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache)
end

if isa(id, PkgId)
curr_prefs_hash = get_preferences_hash(id.uuid, cache)
if prefs_hash != curr_prefs_hash
@debug "Rejecting cache file $cachefile because preferences hash does not match 0x$(string(prefs_hash, base=16)) != 0x$(string(curr_prefs_hash, base=16))"
return true
end

get!(PkgOrigin, pkgorigins, id).cachepath = cachefile
end

Expand Down
1 change: 1 addition & 0 deletions base/sysimg.jl
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ let
:Distributed,
:SharedArrays,
:TOML,
:CompilePreferences,
:Artifacts,
:Pkg,
:Test,
Expand Down
2 changes: 2 additions & 0 deletions base/util.jl
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,8 @@ _crc32c(io::IO, crc::UInt32=0x00000000) = _crc32c(io, typemax(Int64), crc)
_crc32c(io::IOStream, crc::UInt32=0x00000000) = _crc32c(io, filesize(io)-position(io), crc)
_crc32c(uuid::UUID, crc::UInt32=0x00000000) =
ccall(:jl_crc32c, UInt32, (UInt32, Ref{UInt128}, Csize_t), crc, uuid.value, 16)
_crc32c(x::Integer, crc::UInt32=0x00000000) =
ccall(:jl_crc32c, UInt32, (UInt32, Vector{UInt8}, Csize_t), crc, reinterpret(UInt8, [x]), sizeof(x))

"""
@kwdef typedef
Expand Down
25 changes: 25 additions & 0 deletions src/dump.c
Original file line number Diff line number Diff line change
Expand Up @@ -1123,6 +1123,31 @@ static int64_t write_dependency_list(ios_t *s, jl_array_t **udepsp, jl_array_t *
write_int32(s, 0);
}
write_int32(s, 0); // terminator, for ease of reading

// Calculate CompilePreferences hash for current package.
jl_value_t *prefs_hash = NULL;
if (jl_base_module) {
// Toplevel module is the module we're currently compiling, use it to get our preferences hash
jl_value_t * toplevel = (jl_value_t*)jl_get_global(jl_base_module, jl_symbol("__toplevel__"));
jl_value_t * prefs_hash_func = jl_get_global(jl_base_module, jl_symbol("get_preferences_hash"));

if (toplevel && prefs_hash_func) {
// call get_preferences_hash(__toplevel__)
jl_value_t *prefs_hash_args[2] = {prefs_hash_func, (jl_value_t*)toplevel};
size_t last_age = jl_get_ptls_states()->world_age;
jl_get_ptls_states()->world_age = jl_world_counter;
prefs_hash = (jl_value_t*)jl_apply(prefs_hash_args, 2);
jl_get_ptls_states()->world_age = last_age;
}
}

// If we successfully got the preferences, write it out, otherwise write `0` for this `.ji` file.
if (prefs_hash != NULL) {
write_uint64(s, jl_unbox_uint64(prefs_hash));
} else {
write_uint64(s, 0);
}

// write a dummy file position to indicate the beginning of the source-text
pos = ios_pos(s);
ios_seek(s, initial_pos);
Expand Down
12 changes: 12 additions & 0 deletions stdlib/CompilePreferences/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name = "CompilePreferences"
uuid = "21216c6a-2e73-6563-6e65-726566657250"

[deps]
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"

[extras]
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test", "Pkg"]
46 changes: 46 additions & 0 deletions stdlib/CompilePreferences/docs/src/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# CompilePreferences

!!! compat "Julia 1.6"
Julia's `CompilePreferences` API requires at least Julia 1.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.

## API Overview

`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.

Example usage:

```julia
using CompilePreferences

function get_preferred_backend()
prefs = @load_preferences()
return get(prefs, "backend", "native")
end

function set_backend(new_backend)
@modify_preferences!() do prefs
prefs["backend"] = new_backend
end
end
```

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.
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.

# API Reference

!!! compat "Julia 1.6"
Julia's `CompilePreferences` API requires at least Julia 1.6.

```@docs
CompilePreferences.load_preferences
CompilePreferences.@load_preferences
CompilePreferences.save_preferences!
CompilePreferences.@save_preferences!
CompilePreferences.modify_preferences!
CompilePreferences.@modify_preferences!
CompilePreferences.clear_preferences!
CompilePreferences.@clear_preferences!
```
Loading

0 comments on commit b0f5abe

Please sign in to comment.