Skip to content

Commit 7d5dbf5

Browse files
authored
Artifacts: avoid reflection hacks to access ensure_artifact_installed (#37844)
Packages should never access Base.loaded_modules() to call functions from it, as it can be brittle and create future incompatibilities, so instead we require the user to explicitly declare a dependency on the lazy-download machinery, if they requiring the ability to use it (for lazy artifacts). As a deprecation, if the user has `using Pkg`, that will be used instead, with a depwarn.
1 parent c701c32 commit 7d5dbf5

File tree

13 files changed

+161
-51
lines changed

13 files changed

+161
-51
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ Standard library changes
150150
* The `Pkg.Artifacts` module has been imported as a separate standard library. It is still available as
151151
`Pkg.Artifacts`, however starting from Julia v1.6+, packages may import simply `Artifacts` without importing
152152
all of `Pkg` alongside ([#37320]).
153+
* To download artifacts lazily, `LazyArtifacts` now must be explicitly listed as a dependency, to avoid needing the
154+
support machinery to be available when it is not commonly needed ([#37844]).
153155
* `@time` now reports if the time presented included any compilation time, which is shown as a percentage ([#37678]).
154156
* `@varinfo` can now report non-exported objects within modules, look recursively into submodules, and return a sorted
155157
results table ([#38042]).

base/sysimg.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ let
6868

6969
# 4-depth packages
7070
:Pkg,
71+
72+
# 5-depth packages
73+
:LazyArtifacts,
7174
]
7275
maxlen = reduce(max, textwidth.(string.(stdlibs)); init=0)
7376

stdlib/Artifacts/src/Artifacts.jl

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ function jointail(dir, tail)
524524
end
525525
end
526526

527-
function _artifact_str(__module__, artifacts_toml, name, path_tail, artifact_dict, hash, platform)
527+
function _artifact_str(__module__, artifacts_toml, name, path_tail, artifact_dict, hash, platform, @nospecialize(lazyartifacts))
528528
if haskey(Base.module_keys, __module__)
529529
# Process overrides for this UUID, if we know what it is
530530
process_overrides(artifact_dict, Base.module_keys[__module__].uuid)
@@ -538,10 +538,16 @@ function _artifact_str(__module__, artifacts_toml, name, path_tail, artifact_dic
538538
end
539539
end
540540

541-
# If not, we need to download it. We look up the Pkg module through `Base.loaded_modules()`
542-
# then invoke `ensure_artifact_installed()`:
543-
Pkg = first(filter(p-> p[1].name == "Pkg", Base.loaded_modules))[2]
544-
return jointail(Pkg.Artifacts.ensure_artifact_installed(string(name), artifacts_toml; platform), path_tail)
541+
# If not, try determining what went wrong:
542+
meta = artifact_meta(name, artifact_dict, artifacts_toml; platform)
543+
if meta !== nothing && get(meta, "lazy", false)
544+
if lazyartifacts isa Module && isdefined(lazyartifacts, :ensure_artifact_installed)
545+
nameof(lazyartifacts) === :Pkg && Base.depwarn("using Pkg instead of using LazyArtifacts is deprecated", :var"@artifact_str", force=true)
546+
return jointail(lazyartifacts.ensure_artifact_installed(string(name), artifacts_toml; platform), path_tail)
547+
end
548+
error("Artifact $(repr(name)) is a lazy artifact; package developers must call `using LazyArtifacts` in $(__module__) before using lazy artifacts.")
549+
end
550+
error("Artifact $(repr(name)) was not installed correctly. Try `using Pkg; Pkg.instantiate()` to re-install all missing resources.")
545551
end
546552

547553
"""
@@ -607,11 +613,15 @@ end
607613
"""
608614
macro artifact_str(name)
609615
610-
Macro that is used to automatically ensure an artifact is installed, and return its
611-
location on-disk. Automatically looks the artifact up by name in the project's
612-
`(Julia)Artifacts.toml` file. Throws an error on inability to install the requested
613-
artifact. If run in the REPL, searches for the toml file starting in the current
614-
directory, see `find_artifacts_toml()` for more.
616+
Return the on-disk path to an artifact. Automatically looks the artifact up by
617+
name in the project's `(Julia)Artifacts.toml` file. Throws an error on if the
618+
requested artifact is not present. If run in the REPL, searches for the toml
619+
file starting in the current directory, see `find_artifacts_toml()` for more.
620+
621+
If the artifact is marked "lazy" and the package has `using LazyArtifacts`
622+
defined, the artifact will be downloaded on-demand with `Pkg` the first time
623+
this macro tries to compute the path. The files will then be left installed
624+
locally for later.
615625
616626
If `name` contains a forward or backward slash, all elements after the first slash will
617627
be taken to be path names indexing into the artifact, allowing for an easy one-liner to
@@ -649,14 +659,20 @@ macro artifact_str(name, platform=nothing)
649659
# Invalidate calling .ji file if Artifacts.toml file changes
650660
Base.include_dependency(artifacts_toml)
651661

662+
# Check if the user has provided `LazyArtifacts`, and thus supports lazy artifacts
663+
lazyartifacts = isdefined(__module__, :LazyArtifacts) ? GlobalRef(__module__, :LazyArtifacts) : nothing
664+
if lazyartifacts === nothing && isdefined(__module__, :Pkg)
665+
lazyartifacts = GlobalRef(__module__, :Pkg) # deprecated
666+
end
667+
652668
# If `name` is a constant, (and we're using the default `Platform`) we can actually load
653669
# and parse the `Artifacts.toml` file now, saving the work from runtime.
654670
if isa(name, AbstractString) && platform === nothing
655671
# To support slash-indexing, we need to split the artifact name from the path tail:
656672
platform = HostPlatform()
657-
local artifact_name, artifact_path_tail, hash = artifact_slash_lookup(name, artifact_dict, artifacts_toml, platform)
673+
artifact_name, artifact_path_tail, hash = artifact_slash_lookup(name, artifact_dict, artifacts_toml, platform)
658674
return quote
659-
Base.invokelatest(_artifact_str, $(__module__), $(artifacts_toml), $(artifact_name), $(artifact_path_tail), $(artifact_dict), $(hash), $(platform))
675+
Base.invokelatest(_artifact_str, $(__module__), $(artifacts_toml), $(artifact_name), $(artifact_path_tail), $(artifact_dict), $(hash), $(platform), $(lazyartifacts))
660676
end
661677
else
662678
if platform === nothing
@@ -665,7 +681,7 @@ macro artifact_str(name, platform=nothing)
665681
return quote
666682
local platform = $(esc(platform))
667683
local artifact_name, artifact_path_tail, hash = artifact_slash_lookup($(esc(name)), $(artifact_dict), $(artifacts_toml), platform)
668-
Base.invokelatest(_artifact_str, $(__module__), $(artifacts_toml), artifact_name, artifact_path_tail, $(artifact_dict), hash, platform)
684+
Base.invokelatest(_artifact_str, $(__module__), $(artifacts_toml), artifact_name, artifact_path_tail, $(artifact_dict), hash, platform, $(lazyartifacts))
669685
end
670686
end
671687
end

stdlib/Artifacts/test/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/artifacts/
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using Artifacts: with_artifacts_directory
2+
# using Pkg.Artifacts: ensure_all_artifacts_installed
3+
using Pkg.Artifacts: load_artifacts_toml, ensure_artifact_installed
4+
let
5+
tempdir = joinpath(@__DIR__, "artifacts")
6+
toml = joinpath(@__DIR__, "Artifacts.toml")
7+
unused = Base.BinaryPlatforms.Platform(string(Sys.ARCH), "linux")
8+
with_artifacts_directory(tempdir) do
9+
# ensure_all_artifacts_installed(toml; include_lazy=false)
10+
dict = load_artifacts_toml(toml)
11+
for (name, meta) in dict
12+
if meta isa Array
13+
for meta in meta
14+
get(meta, "lazy", false) && continue
15+
ensure_artifact_installed(name, meta, toml; platform=unused)
16+
end
17+
else; meta::Dict
18+
get(meta, "lazy", false) && continue
19+
ensure_artifact_installed(name, meta, toml; platform=unused)
20+
end
21+
end
22+
end
23+
end

stdlib/Artifacts/test/runtests.jl

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
using Artifacts, Test, Base.BinaryPlatforms
44
using Artifacts: with_artifacts_directory, pack_platform!, unpack_platform
55

6+
# prepare for the package tests by ensuring the required artifacts are downloaded now
7+
run(addenv(`$(Base.julia_cmd()) --color=no $(joinpath(@__DIR__, "refresh_artifacts.jl"))`, "TERM"=>"dumb"))
8+
69
@testset "Artifact Paths" begin
710
mktempdir() do tempdir
811
with_artifacts_directory(tempdir) do
@@ -78,45 +81,43 @@ end
7881
end
7982

8083
@testset "Artifact Slash-indexing" begin
81-
mktempdir() do tempdir
82-
with_artifacts_directory(tempdir) do
83-
exeext = Sys.iswindows() ? ".exe" : ""
84-
85-
# simple lookup, gives us the directory for `c_simple` for the current architecture
86-
c_simple_dir = artifact"c_simple"
87-
@test isdir(c_simple_dir)
88-
c_simple_exe_path = joinpath(c_simple_dir, "bin", "c_simple$(exeext)")
89-
@test isfile(c_simple_exe_path)
90-
91-
# Simple slash-indexed lookup
92-
c_simple_bin_path = artifact"c_simple/bin"
93-
@test isdir(c_simple_bin_path)
94-
# Test that forward and backward slash are equivalent
95-
@test artifact"c_simple\\bin" == artifact"c_simple/bin"
96-
97-
# Dynamically-computed lookup; not done at compile-time
98-
generate_artifact_name() = "c_simple"
99-
c_simple_dir = @artifact_str(generate_artifact_name())
100-
@test isdir(c_simple_dir)
101-
c_simple_exe_path = joinpath(c_simple_dir, "bin", "c_simple$(exeext)")
102-
@test isfile(c_simple_exe_path)
103-
104-
# Dynamically-computed slash-indexing:
105-
generate_bin_path(pathsep) = "c_simple$(pathsep)bin$(pathsep)c_simple$(exeext)"
106-
@test isfile(@artifact_str(generate_bin_path("/")))
107-
@test isfile(@artifact_str(generate_bin_path("\\")))
108-
end
84+
tempdir = joinpath(@__DIR__, "artifacts")
85+
with_artifacts_directory(tempdir) do
86+
exeext = Sys.iswindows() ? ".exe" : ""
87+
88+
# simple lookup, gives us the directory for `c_simple` for the current architecture
89+
c_simple_dir = artifact"c_simple"
90+
@test isdir(c_simple_dir)
91+
c_simple_exe_path = joinpath(c_simple_dir, "bin", "c_simple$(exeext)")
92+
@test isfile(c_simple_exe_path)
93+
94+
# Simple slash-indexed lookup
95+
c_simple_bin_path = artifact"c_simple/bin"
96+
@test isdir(c_simple_bin_path)
97+
# Test that forward and backward slash are equivalent
98+
@test artifact"c_simple\\bin" == artifact"c_simple/bin"
99+
100+
# Dynamically-computed lookup; not done at compile-time
101+
generate_artifact_name() = "c_simple"
102+
c_simple_dir = @artifact_str(generate_artifact_name())
103+
@test isdir(c_simple_dir)
104+
c_simple_exe_path = joinpath(c_simple_dir, "bin", "c_simple$(exeext)")
105+
@test isfile(c_simple_exe_path)
106+
107+
# Dynamically-computed slash-indexing:
108+
generate_bin_path(pathsep) = "c_simple$(pathsep)bin$(pathsep)c_simple$(exeext)"
109+
@test isfile(@artifact_str(generate_bin_path("/")))
110+
@test isfile(@artifact_str(generate_bin_path("\\")))
109111
end
110112
end
111113

112114
@testset "@artifact_str Platform passing" begin
113-
mktempdir() do tempdir
114-
with_artifacts_directory(tempdir) do
115-
win64 = Platform("x86_64", "windows")
116-
mac64 = Platform("x86_64", "macos")
117-
@test basename(@artifact_str("c_simple", win64)) == "444cecb70ff39e8961dd33e230e151775d959f37"
118-
@test basename(@artifact_str("c_simple", mac64)) == "7ba74e239348ea6c060f994c083260be3abe3095"
119-
end
115+
tempdir = joinpath(@__DIR__, "artifacts")
116+
with_artifacts_directory(tempdir) do
117+
win64 = Platform("x86_64", "windows")
118+
mac64 = Platform("x86_64", "macos")
119+
@test basename(@artifact_str("c_simple", win64)) == "444cecb70ff39e8961dd33e230e151775d959f37"
120+
@test basename(@artifact_str("c_simple", mac64)) == "7ba74e239348ea6c060f994c083260be3abe3095"
120121
end
121122
end
122123

@@ -131,3 +132,14 @@ end
131132
@test artifacts["c_simple"]["git-tree-sha1"] == "0c509b3302db90a9393d6036c3ffcd14d190523d"
132133
@test artifacts["socrates"]["git-tree-sha1"] == "43563e7631a7eafae1f9f8d9d332e3de44ad7239"
133134
end
135+
136+
@testset "@artifact_str install errors" begin
137+
mktempdir() do tempdir
138+
with_artifacts_directory(tempdir) do
139+
ex = @test_throws ErrorException artifact"c_simple"
140+
@test startswith(ex.value.msg, "Artifact \"c_simple\" was not installed correctly. ")
141+
ex = @test_throws ErrorException artifact"socrates"
142+
@test startswith(ex.value.msg, "Artifact \"socrates\" is a lazy artifact; ")
143+
end
144+
end
145+
end

stdlib/LazyArtifacts/Project.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name = "LazyArtifacts"
2+
uuid = "4af54fe1-eca0-43a8-85a7-787d91b784e3"
3+
4+
[deps]
5+
Artifacts = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
6+
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
7+
8+
[extras]
9+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
10+
11+
[targets]
12+
test = ["Test"]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# This file is a part of Julia. License is MIT: https://julialang.org/license
2+
3+
module LazyArtifacts
4+
5+
# reexport the Artifacts API
6+
using Artifacts: Artifacts,
7+
artifact_exists, artifact_path, artifact_meta, artifact_hash,
8+
select_downloadable_artifacts, find_artifacts_toml, @artifact_str
9+
export artifact_exists, artifact_path, artifact_meta, artifact_hash,
10+
select_downloadable_artifacts, find_artifacts_toml, @artifact_str
11+
12+
# define a function for satisfying lazy Artifact downloads
13+
using Pkg.Artifacts: ensure_artifact_installed
14+
15+
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../Artifacts/test/Artifacts.toml

stdlib/LazyArtifacts/test/runtests.jl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using LazyArtifacts
2+
using Test
3+
4+
mktempdir() do tempdir
5+
LazyArtifacts.Artifacts.with_artifacts_directory(tempdir) do
6+
socrates_dir = artifact"socrates"
7+
@test isdir(socrates_dir)
8+
ex = @test_throws ErrorException artifact"c_simple"
9+
@test startswith(ex.value.msg, "Artifact \"c_simple\" was not installed correctly. ")
10+
end
11+
end
12+
13+
# Need to set depwarn flag before testing deprecations
14+
@test success(run(setenv(`$(Base.julia_cmd()) --depwarn=no --startup-file=no -e '
15+
using Artifacts, Pkg
16+
using Test
17+
mktempdir() do tempdir
18+
Artifacts.with_artifacts_directory(tempdir) do
19+
socrates_dir = @test_logs(
20+
(:warn, "using Pkg instead of using LazyArtifacts is deprecated"),
21+
artifact"socrates")
22+
@test isdir(socrates_dir)
23+
end
24+
end'`,
25+
dir=@__DIR__)))

0 commit comments

Comments
 (0)