Skip to content

Commit fcd4c3c

Browse files
nhz2KristofferC
authored andcommitted
Avoid deleting existing artifacts when ignoring hashes. (#3768)
* avoid deleting existing artifacts * fix order * add testset
1 parent 78151b1 commit fcd4c3c

File tree

2 files changed

+104
-54
lines changed

2 files changed

+104
-54
lines changed

src/Artifacts.jl

Lines changed: 78 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,7 @@ function create_artifact(f::Function)
4949
# as something that was foolishly overridden. This should be virtually impossible
5050
# unless the user has been very unwise, but let's be cautious.
5151
new_path = artifact_path(artifact_hash; honor_overrides=false)
52-
if !isdir(new_path)
53-
# Move this generated directory to its final destination, set it to read-only
54-
mv(temp_dir, new_path)
55-
chmod(new_path, filemode(dirname(new_path)))
56-
set_readonly(new_path)
57-
end
52+
_mv_temp_artifact_dir(temp_dir, new_path)
5853

5954
# Give the people what they want
6055
return artifact_hash
@@ -64,6 +59,28 @@ function create_artifact(f::Function)
6459
end
6560
end
6661

62+
"""
63+
_mv_temp_artifact_dir(temp_dir::String, new_path::String)::Nothing
64+
Either rename the directory at `temp_dir` to `new_path` and set it to read-only
65+
or if `new_path` artifact already exists try to do nothing.
66+
"""
67+
function _mv_temp_artifact_dir(temp_dir::String, new_path::String)::Nothing
68+
if !isdir(new_path)
69+
# This next step is like
70+
# `mv(temp_dir, new_path)`.
71+
# However, `mv` defaults to `cp` if `rename` returns an error.
72+
# `cp` is not atomic, so avoid the potential of calling it.
73+
err = ccall(:jl_fs_rename, Int32, (Cstring, Cstring), temp_dir, new_path)
74+
# Ignore rename error, but ensure `new_path` exists.
75+
if !isdir(new_path)
76+
error("$(repr(new_path)) could not be made")
77+
end
78+
chmod(new_path, filemode(dirname(new_path)))
79+
set_readonly(new_path)
80+
end
81+
nothing
82+
end
83+
6784
"""
6885
remove_artifact(hash::SHA1; honor_overrides::Bool=false)
6986
@@ -289,68 +306,75 @@ function download_artifact(
289306
return true
290307
end
291308

292-
# We download by using `create_artifact()`. We do this because the download may
309+
# Ensure the `artifacts` directory exists in our default depot
310+
artifacts_dir = first(artifacts_dirs())
311+
mkpath(artifacts_dir)
312+
# expected artifact path
313+
dst = joinpath(artifacts_dir, bytes2hex(tree_hash.bytes))
314+
315+
# We download by using a temporary directory. We do this because the download may
293316
# be corrupted or even malicious; we don't want to clobber someone else's artifact
294317
# by trusting the tree hash that has been given to us; we will instead download it
295318
# to a temporary directory, calculate the true tree hash, then move it to the proper
296319
# location only after knowing what it is, and if something goes wrong in the process,
297-
# everything should be cleaned up. Luckily, that is precisely what our
298-
# `create_artifact()` wrapper does, so we use that here.
299-
calc_hash = try
300-
create_artifact() do dir
301-
download_verify_unpack(tarball_url, tarball_hash, dir, ignore_existence=true, verbose=verbose,
320+
# everything should be cleaned up.
321+
322+
# Temporary directory where we'll do our creation business
323+
temp_dir = mktempdir(artifacts_dir)
324+
325+
try
326+
download_verify_unpack(tarball_url, tarball_hash, temp_dir, ignore_existence=true, verbose=verbose,
302327
quiet_download=quiet_download, io=io)
328+
calc_hash = SHA1(GitTools.tree_hash(temp_dir))
329+
330+
# Did we get what we expected? If not, freak out.
331+
if calc_hash.bytes != tree_hash.bytes
332+
msg = """
333+
Tree Hash Mismatch!
334+
Expected git-tree-sha1: $(bytes2hex(tree_hash.bytes))
335+
Calculated git-tree-sha1: $(bytes2hex(calc_hash.bytes))
336+
"""
337+
# Since tree hash calculation is rather fragile and file system dependent,
338+
# we allow setting JULIA_PKG_IGNORE_HASHES=1 to ignore the error and move
339+
# the artifact to the expected location and return true
340+
ignore_hash_env_set = get(ENV, "JULIA_PKG_IGNORE_HASHES", "") != ""
341+
if ignore_hash_env_set
342+
ignore_hash = Base.get_bool_env("JULIA_PKG_IGNORE_HASHES", false)
343+
ignore_hash === nothing && @error(
344+
"Invalid ENV[\"JULIA_PKG_IGNORE_HASHES\"] value",
345+
ENV["JULIA_PKG_IGNORE_HASHES"],
346+
)
347+
ignore_hash = something(ignore_hash, false)
348+
else
349+
# default: false except Windows users who can't symlink
350+
ignore_hash = Sys.iswindows() &&
351+
!mktempdir(can_symlink, artifacts_dir)
352+
end
353+
if ignore_hash
354+
desc = ignore_hash_env_set ?
355+
"Environment variable \$JULIA_PKG_IGNORE_HASHES is true" :
356+
"System is Windows and user cannot create symlinks"
357+
msg *= "\n$desc: \
358+
ignoring hash mismatch and moving \
359+
artifact to the expected location"
360+
@error(msg)
361+
else
362+
error(msg)
363+
end
303364
end
365+
# Move it to the location we expected
366+
_mv_temp_artifact_dir(temp_dir, dst)
304367
catch err
305368
@debug "download_artifact error" tree_hash tarball_url tarball_hash err
306369
if isa(err, InterruptException)
307370
rethrow(err)
308371
end
309372
# If something went wrong during download, return the error
310373
return err
374+
finally
375+
# Always attempt to cleanup
376+
rm(temp_dir; recursive=true, force=true)
311377
end
312-
313-
# Did we get what we expected? If not, freak out.
314-
if calc_hash.bytes != tree_hash.bytes
315-
msg = """
316-
Tree Hash Mismatch!
317-
Expected git-tree-sha1: $(bytes2hex(tree_hash.bytes))
318-
Calculated git-tree-sha1: $(bytes2hex(calc_hash.bytes))
319-
"""
320-
# actual and expected artifiact paths
321-
src = artifact_path(calc_hash; honor_overrides=false)
322-
dst = artifact_path(tree_hash; honor_overrides=false)
323-
# Since tree hash calculation is rather fragile and file system dependent,
324-
# we allow setting JULIA_PKG_IGNORE_HASHES=1 to ignore the error and move
325-
# the artifact to the expected location and return true
326-
ignore_hash_env_set = get(ENV, "JULIA_PKG_IGNORE_HASHES", "") != ""
327-
if ignore_hash_env_set
328-
ignore_hash = Base.get_bool_env("JULIA_PKG_IGNORE_HASHES", false)
329-
ignore_hash === nothing && @error(
330-
"Invalid ENV[\"JULIA_PKG_IGNORE_HASHES\"] value",
331-
ENV["JULIA_PKG_IGNORE_HASHES"],
332-
)
333-
ignore_hash = something(ignore_hash, false)
334-
else
335-
# default: false except Windows users who can't symlink
336-
ignore_hash = Sys.iswindows() &&
337-
!mktempdir(can_symlink, dirname(src))
338-
end
339-
if ignore_hash
340-
desc = ignore_hash_env_set ?
341-
"Environment variable \$JULIA_PKG_IGNORE_HASHES is true" :
342-
"System is Windows and user cannot create symlinks"
343-
msg *= "\n$desc: \
344-
ignoring hash mismatch and moving \
345-
artifact to the expected location"
346-
@error(msg)
347-
# Move it to the location we expected
348-
mv(src, dst; force=true)
349-
return true
350-
end
351-
return ErrorException(msg)
352-
end
353-
354378
return true
355379
end
356380

test/artifacts.jl

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,4 +799,30 @@ end
799799
end
800800
end
801801

802+
@testset "installing artifacts when symlinks are copied" begin
803+
# copy symlinks to simulate the typical Microsoft Windows user experience where
804+
# developer mode is not enabled (no admin rights)
805+
withenv("BINARYPROVIDER_COPYDEREF"=>"true", "JULIA_PKG_IGNORE_HASHES"=>"true") do
806+
temp_pkg_dir() do tmpdir
807+
artifacts_toml = joinpath(tmpdir, "Artifacts.toml")
808+
cp(joinpath(@__DIR__, "test_packages", "ArtifactInstallation", "Artifacts.toml"), artifacts_toml)
809+
Pkg.activate(tmpdir)
810+
cts_real_hash = create_artifact() do dir
811+
local meta = Artifacts.artifact_meta("collapse_the_symlink", artifacts_toml)
812+
local collapse_url = meta["download"][1]["url"]
813+
local collapse_hash = meta["download"][1]["sha256"]
814+
# Because "BINARYPROVIDER_COPYDEREF"=>"true", this will copy symlinks.
815+
download_verify_unpack(collapse_url, collapse_hash, dir; verbose=true, ignore_existence=true)
816+
end
817+
cts_hash = artifact_hash("collapse_the_symlink", artifacts_toml)
818+
@test !artifact_exists(cts_hash)
819+
@test artifact_exists(cts_real_hash)
820+
@test_logs (:error, r"Tree Hash Mismatch!") match_mode=:any Pkg.instantiate()
821+
@test artifact_exists(cts_hash)
822+
# Make sure existing artifacts don't get deleted.
823+
@test artifact_exists(cts_real_hash)
824+
end
825+
end
826+
end
827+
802828
end # module

0 commit comments

Comments
 (0)