Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 83 additions & 9 deletions base/precompilation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,73 @@ function collect_all_deps(direct_deps, dep, alldeps=Set{Base.PkgId}())
end


"""
precompilepkgs(pkgs; kwargs...)

Precompile packages and their dependencies, with support for parallel compilation,
progress tracking, and various compilation configurations.

`pkgs::Union{Vector{String}, Vector{PkgId}}`: Packages to precompile. When
empty (default), precompiles all project dependencies. When specified,
precompiles only the given packages and their dependencies (unless
`manifest=true`).

# Keyword Arguments
- `internal_call::Bool`: Indicates this is an automatic/internal precompilation call
(e.g., triggered by package loading). When `true`, errors are handled gracefully: in
interactive sessions, errors are stored in `Base.MainInclude.err` instead of throwing;
in non-interactive sessions, errors are printed but not thrown. Default: `false`.

- `strict::Bool`: Controls error reporting scope. When `false` (default), only reports
errors for direct project dependencies.

- `warn_loaded::Bool`: When `true` (default), checks for and warns about packages that are
precompiled but already loaded with a different version. Displays a warning that Julia
needs to be restarted to use the newly precompiled versions.

- `timing::Bool`: When `true` (not default), displays timing information for
each package compilation, but only if compilation might have succeeded.
Disables fancy progress bar output (timing is shown in simple text mode).

- `_from_loading::Bool`: Internal flag indicating the call originated from the
package loading system. When `true` (not default): returns early instead of
throwing when packages are not found; suppresses progress messages when not
in an interactive session; allows packages outside the current environment to
be added as serial precompilation jobs; skips LOADING_CACHE initialization;
and changes cachefile locking behavior.

- `configs::Union{Config,Vector{Config}}`: Compilation configurations to use. Each Config
is a `Pair{Cmd, Base.CacheFlags}` specifying command flags and cache flags. When
multiple configs are provided, each package is precompiled for each configuration.

- `io::IO`: Output stream for progress messages, warnings, and errors. Can be
redirected (e.g., to `devnull` when called from loading in non-interactive mode).

- `fancyprint::Bool`: Controls output format. When `true`, displays an animated progress
bar with spinners. When `false`, instead enables `timing` mode. Automatically
disabled when `timing=true` or when called from loading in non-interactive mode.

- `manifest::Bool`: Controls the scope of packages to precompile. When `false` (default),
precompiles only packages specified in `pkgs` and their dependencies. When `true`,
precompiles all packages in the manifest (workspace mode), typically used by Pkg for
workspace precompile requests.

- `ignore_loaded::Bool`: Controls whether already-loaded packages affect cache
freshness checks. When `false` (not default), loaded package versions are considered when
determining if cache files are fresh.

# Return
- `Vector{String}`: Paths to cache files for the requested packages.
- `Nothing`: precompilation should be skipped

# Notes
- Packages in circular dependency cycles are skipped with a warning.
- Packages with `__precompile__(false)` are skipped if they are from loading to
avoid repeated work on every session.
- Parallel compilation is controlled by `JULIA_NUM_PRECOMPILE_TASKS` environment variable
(defaults to CPU_THREADS + 1, capped at 16, halved on Windows).
- Extensions are precompiled when all their triggers are available in the environment.
"""
function precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}=String[];
internal_call::Bool=false,
strict::Bool = false,
Expand Down Expand Up @@ -745,8 +812,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}},
pkg_names = [pkg.name for pkg in project_deps]
end
keep = Set{Base.PkgId}()
for dep in direct_deps
dep_pkgid = first(dep)
for dep_pkgid in keys(direct_deps)
if dep_pkgid.name in pkg_names
push!(keep, dep_pkgid)
collect_all_deps(direct_deps, dep_pkgid, keep)
Expand Down Expand Up @@ -990,8 +1056,10 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}},
notify(was_processed[pkg_config])
continue
end
# Heuristic for when precompilation is disabled
if occursin(r"\b__precompile__\(\s*false\s*\)", read(sourcepath, String))
# Heuristic for when precompilation is disabled, which must not over-estimate however for any dependent
# since it will also block precompilation of all dependents
if _from_loading && single_requested_pkg && occursin(r"\b__precompile__\(\s*false\s*\)", read(sourcepath, String))
Base.@logmsg logcalls "Disabled precompiling $(repr("text/plain", pkg)) since the text `__precompile__(false)` was found in file."
notify(was_processed[pkg_config])
continue
end
Expand Down Expand Up @@ -1035,7 +1103,13 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}},
end
# for extensions, any extension in our direct dependencies is one we have a right to load
# for packages, we may load any extension (all possible triggers are accounted for above)
loadable_exts = haskey(ext_to_parent, pkg) ? filter((dep)->haskey(ext_to_parent, dep), direct_deps[pkg]) : nothing
loadable_exts = haskey(ext_to_parent, pkg) ? filter((dep)->haskey(ext_to_parent, dep), deps) : nothing
if !isempty(deps)
# if deps is empty, either it doesn't have any (so compiled-modules is
# irrelevant) or we couldn't compute them (so we actually should attempt
# serial compile, as the dependencies are not in the parallel list)
flags = `$flags --compiled-modules=strict`
end
if _from_loading && pkg in requested_pkgids
# loading already took the cachefile_lock and printed logmsg for its explicit requests
t = @elapsed ret = begin
Expand Down Expand Up @@ -1223,18 +1297,18 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}},
pluralde = n_direct_errs == 1 ? "y" : "ies"
direct = strict ? "" : "direct "
err_msg = "The following $n_direct_errs $(direct)dependenc$(pluralde) failed to precompile:\n$(String(take!(err_str)))"
if internal_call # aka. auto-precompilation
if isinteractive()
if internal_call # aka. decide which untested code path to run that does some unsafe behavior
if isinteractive() # XXX: this test is incorrect
plural1 = length(failed_deps) == 1 ? "y" : "ies"
println(io, " ", color_string("$(length(failed_deps))", Base.error_color()), " dependenc$(plural1) errored.")
println(io, " For a report of the errors see `julia> err`. To retry use `pkg> precompile`")
setglobal!(Base.MainInclude, :err, PkgPrecompileError(err_msg))
setglobal!(Base.MainInclude, :err, PkgPrecompileError(err_msg)) # XXX: this call is dangerous
else
# auto-precompilation shouldn't throw but if the user can't easily access the
# error messages, just show them
print(io, "\n", err_msg)
end
else
else # XXX: crashing is wrong
println(io)
throw(PkgPrecompileError(err_msg))
end
Expand Down
4 changes: 1 addition & 3 deletions test/loading.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1376,10 +1376,8 @@ end
""")
write(joinpath(foo_path, "Manifest.toml"),
"""
# This file is machine-generated - editing it directly is not advised
julia_version = "1.13.0-DEV"
julia_version = "1.13.0"
manifest_format = "2.0"
project_hash = "8699765aeeac181c3e5ddbaeb9371968e1f84d6b"

[[deps.Foo51989]]
path = "."
Expand Down
156 changes: 156 additions & 0 deletions test/precompile.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2544,4 +2544,160 @@ let io = IOBuffer()
@test isempty(String(take!(io)))
end

# Test --compiled-modules=strict in precompilepkgs
@testset "compiled-modules=strict with dependencies" begin
mkdepottempdir() do depot
# Create three packages: one that fails to precompile, one that loads it, one that doesn't
project_path = joinpath(depot, "testenv")
mkpath(project_path)

# Create FailPkg - a package that can't be precompiled
fail_pkg_path = joinpath(depot, "dev", "FailPkg")
mkpath(joinpath(fail_pkg_path, "src"))
write(joinpath(fail_pkg_path, "Project.toml"),
"""
name = "FailPkg"
uuid = "10000000-0000-0000-0000-000000000001"
version = "0.1.0"
""")
write(joinpath(fail_pkg_path, "src", "FailPkg.jl"),
"""
module FailPkg
print("FailPkg precompiling.\n")
error("fail")
end
""")

# Create LoadsFailPkg - depends on and loads FailPkg (should fail with strict)
loads_pkg_path = joinpath(depot, "dev", "LoadsFailPkg")
mkpath(joinpath(loads_pkg_path, "src"))
write(joinpath(loads_pkg_path, "Project.toml"),
"""
name = "LoadsFailPkg"
uuid = "20000000-0000-0000-0000-000000000002"
version = "0.1.0"

[deps]
FailPkg = "10000000-0000-0000-0000-000000000001"
""")
write(joinpath(loads_pkg_path, "src", "LoadsFailPkg.jl"),
"""
module LoadsFailPkg
print("LoadsFailPkg precompiling.\n")
import FailPkg
print("unreachable\n")
end
""")

# Create DependsOnly - depends on FailPkg but doesn't load it (should succeed)
depends_pkg_path = joinpath(depot, "dev", "DependsOnly")
mkpath(joinpath(depends_pkg_path, "src"))
write(joinpath(depends_pkg_path, "Project.toml"),
"""
name = "DependsOnly"
uuid = "30000000-0000-0000-0000-000000000003"
version = "0.1.0"

[deps]
FailPkg = "10000000-0000-0000-0000-000000000001"
""")
write(joinpath(depends_pkg_path, "src", "DependsOnly.jl"),
"""
module DependsOnly
# Has FailPkg as a dependency but doesn't load it
print("DependsOnly precompiling.\n")
end
""")

# Create main project with all packages
write(joinpath(project_path, "Project.toml"),
"""
[deps]
LoadsFailPkg = "20000000-0000-0000-0000-000000000002"
DependsOnly = "30000000-0000-0000-0000-000000000003"
""")
write(joinpath(project_path, "Manifest.toml"),
"""
julia_version = "1.13.0"
manifest_format = "2.0"

[[DependsOnly]]
deps = ["FailPkg"]
uuid = "30000000-0000-0000-0000-000000000003"
version = "0.1.0"

[[FailPkg]]
uuid = "10000000-0000-0000-0000-000000000001"
version = "0.1.0"

[[LoadsFailPkg]]
deps = ["FailPkg"]
uuid = "20000000-0000-0000-0000-000000000002"
version = "0.1.0"

[[deps.DependsOnly]]
deps = ["FailPkg"]
path = "../dev/DependsOnly/"
uuid = "30000000-0000-0000-0000-000000000003"
version = "0.1.0"

[[deps.FailPkg]]
path = "../dev/FailPkg/"
uuid = "10000000-0000-0000-0000-000000000001"
version = "0.1.0"

[[deps.LoadsFailPkg]]
deps = ["FailPkg"]
path = "../dev/LoadsFailPkg/"
uuid = "20000000-0000-0000-0000-000000000002"
version = "0.1.0"
""")

# Call precompilepkgs with output redirected to a file
LoadsFailPkg_output = joinpath(depot, "LoadsFailPkg_output.txt")
DependsOnly_output = joinpath(depot, "DependsOnly_output.txt")
original_depot_path = copy(Base.DEPOT_PATH)
old_proj = Base.active_project()
try
push!(empty!(DEPOT_PATH), depot)
Base.set_active_project(project_path)
loadsfailpkg = open(LoadsFailPkg_output, "w") do io
# set internal_call to bypass buggy code
Base.Precompilation.precompilepkgs(["LoadsFailPkg"]; io, fancyprint=true, internal_call=true)
end
@test isempty(loadsfailpkg::Vector{String})
dependsonly = open(DependsOnly_output, "w") do io
# set internal_call to bypass buggy code
Base.Precompilation.precompilepkgs(["DependsOnly"]; io, fancyprint=true, internal_call=true)
end
@test length(dependsonly::Vector{String}) == 1
finally
Base.set_active_project(old_proj)
append!(empty!(DEPOT_PATH), original_depot_path)
end

output = read(LoadsFailPkg_output, String)
# LoadsFailPkg should fail because it tries to load FailPkg with --compiled-modules=strict
@test_broken count(output, "ERROR: fail") > 0
@test_broken count(output, "ERROR: fail") == 1
@test count("✗ FailPkg", output) > 0
@test count("✗ LoadsFailPkg", output) > 0
@test count("FailPkg precompiling.", output) > 0
@test_broken count("FailPkg precompiling.", output) == 1
@test 0 < count("LoadsFailPkg precompiling.", output) <= 2
@test_broken count("LoadsFailPkg precompiling.", output) == 1
@test !contains(output, "DependsOnly precompiling.")

# DependsOnly should succeed because it doesn't actually load FailPkg
output = read(DependsOnly_output, String)
@test_broken count(output, "ERROR: fail") > 0
@test_broken count(output, "ERROR: fail") == 1
@test count("✗ FailPkg", output) > 0
@test count("Precompiling DependsOnly finished.", output) == 1
@test_broken count("FailPkg precompiling.", output) > 0
@test_broken count("FailPkg precompiling.", output) == 1
@test count("DependsOnly precompiling.", output) == 1
end
end

finish_precompile_test!()