Skip to content

Commit b224be5

Browse files
authored
properly normalize paths before accessing them (#4463)
1 parent 3adcf4a commit b224be5

File tree

6 files changed

+140
-35
lines changed

6 files changed

+140
-35
lines changed

src/API.jl

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import FileWatching
1212

1313
import Base: StaleCacheKey
1414

15-
import ..depots, ..depots1, ..logdir, ..devdir, ..printpkgstyle, .._autoprecompilation_enabled_scoped
15+
import ..depots, ..depots1, ..logdir, ..devdir, ..printpkgstyle, .._autoprecompilation_enabled_scoped, ..manifest_rel_path
1616
import ..Operations, ..GitTools, ..Pkg, ..Registry
17-
import ..can_fancyprint, ..pathrepr, ..isurl, ..PREV_ENV_PATH, ..atomic_toml_write
17+
import ..can_fancyprint, ..pathrepr, ..isurl, ..PREV_ENV_PATH, ..atomic_toml_write, ..safe_realpath
1818
using ..Types, ..TOML
1919
using ..Types: VersionTypes
2020
using Base.BinaryPlatforms
@@ -64,7 +64,7 @@ end
6464
function package_info(env::EnvCache, pkg::PackageSpec, entry::PackageEntry)::PackageInfo
6565
git_source = pkg.repo.source === nothing ? nothing :
6666
isurl(pkg.repo.source::String) ? pkg.repo.source::String :
67-
Operations.project_rel_path(env, pkg.repo.source::String)
67+
safe_realpath(manifest_rel_path(env, pkg.repo.source::String))
6868
_source_path = Operations.source_path(env.manifest_file, pkg)
6969
if _source_path === nothing
7070
@debug "Manifest file $(env.manifest_file) contents:\n$(read(env.manifest_file, String))"
@@ -81,7 +81,7 @@ function package_info(env::EnvCache, pkg::PackageSpec, entry::PackageEntry)::Pac
8181
is_tracking_registry = Operations.is_tracking_registry(pkg),
8282
git_revision = pkg.repo.rev,
8383
git_source = git_source,
84-
source = Operations.project_rel_path(env, _source_path),
84+
source = _source_path,
8585
dependencies = copy(entry.deps), #TODO is copy needed?
8686
)
8787
return info
@@ -250,6 +250,19 @@ function update_source_if_set(env, pkg)
250250
return
251251
end
252252

253+
# Normalize relative paths from user input (pwd-relative) to internal representation (manifest-relative)
254+
# This ensures all relative paths in Pkg are consistently relative to the manifest file
255+
function normalize_package_paths!(ctx::Context, pkgs::Vector{PackageSpec})
256+
for pkg in pkgs
257+
if pkg.repo.source !== nothing && !isurl(pkg.repo.source) && !isabspath(pkg.repo.source)
258+
# User provided a relative path (relative to pwd), convert to manifest-relative
259+
absolute_path = abspath(pkg.repo.source)
260+
pkg.repo.source = Types.relative_project_path(ctx.env.manifest_file, absolute_path)
261+
end
262+
end
263+
return
264+
end
265+
253266
function develop(
254267
ctx::Context, pkgs::Vector{PackageSpec}; shared::Bool = true,
255268
preserve::PreserveLevel = Operations.default_preserve(), platform::AbstractPlatform = HostPlatform(), kwargs...
@@ -285,6 +298,8 @@ function develop(
285298
end
286299
end
287300

301+
normalize_package_paths!(ctx, pkgs)
302+
288303
new_git = handle_repos_develop!(ctx, pkgs, shared)
289304

290305
Operations.update_registries(ctx; force = false, update_cooldown = Day(1))
@@ -337,6 +352,8 @@ function add(
337352
end
338353
end
339354

355+
normalize_package_paths!(ctx, pkgs)
356+
340357
repo_pkgs = PackageSpec[pkg for pkg in pkgs if (pkg.repo.source !== nothing || pkg.repo.rev !== nothing)]
341358
new_git = handle_repos_add!(ctx, repo_pkgs)
342359
# repo + unpinned -> name, uuid, repo.rev, repo.source, tree_hash
@@ -782,7 +799,15 @@ function gc(ctx::Context = Context(); collect_delay::Union{Period, Nothing} = no
782799
end
783800

784801
# Collect the locations of every repo referred to in this manifest
785-
return [Types.add_repo_cache_path(e.repo.source) for (u, e) in manifest if e.repo.source !== nothing]
802+
return [
803+
Types.add_repo_cache_path(
804+
isurl(e.repo.source) ? e.repo.source :
805+
safe_realpath(
806+
isabspath(e.repo.source) ? e.repo.source :
807+
normpath(joinpath(dirname(path), e.repo.source))
808+
)
809+
) for (u, e) in manifest if e.repo.source !== nothing
810+
]
786811
end
787812

788813
function process_artifacts_toml(path, pkgs_to_delete)
@@ -1314,7 +1339,7 @@ function instantiate(
13141339
## Download repo at tree hash
13151340
# determine canonical form of repo source
13161341
if !isurl(repo_source)
1317-
repo_source = normpath(joinpath(dirname(ctx.env.project_file), repo_source))
1342+
repo_source = manifest_rel_path(ctx.env, repo_source)
13181343
end
13191344
if !isurl(repo_source) && !isdir(repo_source)
13201345
pkgerror("Did not find path `$(repo_source)` for $(err_rep(pkg))")

src/Operations.jl

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ using Base.BinaryPlatforms
1616
import ...Pkg
1717
import ...Pkg: pkg_server, Registry, pathrepr, can_fancyprint, printpkgstyle, stderr_f, OFFLINE_MODE
1818
import ...Pkg: UPDATED_REGISTRY_THIS_SESSION, RESPECT_SYSIMAGE_VERSIONS, should_autoprecompile
19-
import ...Pkg: usable_io, discover_repo, create_cachedir_tag
19+
import ...Pkg: usable_io, discover_repo, create_cachedir_tag, manifest_rel_path
2020

2121
#########
2222
# Utils #
@@ -130,7 +130,7 @@ end
130130

131131
function source_path(manifest_file::String, pkg::Union{PackageSpec, PackageEntry}, julia_version = VERSION)
132132
return pkg.tree_hash !== nothing ? find_installed(pkg.name, pkg.uuid, pkg.tree_hash) :
133-
pkg.path !== nothing ? joinpath(dirname(manifest_file), pkg.path) :
133+
pkg.path !== nothing ? normpath(joinpath(dirname(manifest_file), pkg.path)) :
134134
is_or_was_stdlib(pkg.uuid, julia_version) ? Types.stdlib_path(pkg.name) :
135135
nothing
136136
end
@@ -540,7 +540,7 @@ is_tracking_registry(pkg) = !is_tracking_path(pkg) && !is_tracking_repo(pkg)
540540
isfixed(pkg) = !is_tracking_registry(pkg) || pkg.pinned
541541

542542
function collect_developed!(env::EnvCache, pkg::PackageSpec, developed::Vector{PackageSpec})
543-
source = project_rel_path(env, source_path(env.manifest_file, pkg))
543+
source = source_path(env.manifest_file, pkg)
544544
source_env = EnvCache(projectfile_path(source))
545545
pkgs = load_project_deps(source_env.project, source_env.project_file, source_env.manifest, source_env.manifest_file)
546546
for pkg in pkgs
@@ -553,10 +553,7 @@ function collect_developed!(env::EnvCache, pkg::PackageSpec, developed::Vector{P
553553
# otherwise relative to manifest file....
554554
pkg.path = Types.relative_project_path(
555555
env.manifest_file,
556-
project_rel_path(
557-
source_env,
558-
source_path(source_env.manifest_file, pkg)
559-
)
556+
source_path(source_env.manifest_file, pkg)
560557
)
561558
push!(developed, pkg)
562559
collect_developed!(env, pkg, developed)
@@ -607,13 +604,13 @@ function collect_fixed!(env::EnvCache, pkgs::Vector{PackageSpec}, names::Dict{UU
607604
pkg.uuid === nothing && continue
608605
# add repo package if necessary
609606
source = source_path(env.manifest_file, pkg)
610-
path = source === nothing ? nothing : project_rel_path(env, source)
607+
path = source
611608
if (path === nothing || !isdir(path)) && (pkg.repo.rev !== nothing || pkg.repo.source !== nothing)
612609
# ensure revved package is installed
613610
# pkg.tree_hash is set in here
614611
Types.handle_repo_add!(Types.Context(env = env), pkg)
615612
# Recompute path
616-
path = project_rel_path(env, source_path(env.manifest_file, pkg))
613+
path = source_path(env.manifest_file, pkg)
617614
end
618615
if !isdir(path)
619616
# Find which packages depend on this missing package for better error reporting
@@ -1538,7 +1535,6 @@ end
15381535
################################
15391536
# Manifest update and pruning #
15401537
################################
1541-
project_rel_path(env::EnvCache, path::String) = normpath(joinpath(dirname(env.manifest_file), path))
15421538

15431539
function prune_manifest(env::EnvCache)
15441540
# if project uses another manifest, only prune project entry in manifest
@@ -2589,7 +2585,7 @@ end
25892585
function abspath!(env::EnvCache, manifest::Manifest)
25902586
for (uuid, entry) in manifest
25912587
if entry.path !== nothing
2592-
entry.path = project_rel_path(env, entry.path)
2588+
entry.path = manifest_rel_path(env, entry.path)
25932589
end
25942590
end
25952591
return manifest
@@ -2816,7 +2812,7 @@ function test(
28162812
missing_runtests = String[]
28172813
source_paths = String[] # source_path is the package root (not /src)
28182814
for pkg in pkgs
2819-
sourcepath = project_rel_path(ctx.env, source_path(ctx.env.manifest_file, pkg, ctx.julia_version)) # TODO
2815+
sourcepath = source_path(ctx.env.manifest_file, pkg, ctx.julia_version)
28202816
!isfile(testfile(sourcepath)) && push!(missing_runtests, pkg.name)
28212817
push!(source_paths, sourcepath)
28222818
end

src/Types.jl

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -801,8 +801,17 @@ end
801801
function handle_repo_develop!(ctx::Context, pkg::PackageSpec, shared::Bool)
802802
# First, check if we can compute the path easily (which requires a given local path or name)
803803
is_local_path = pkg.repo.source !== nothing && !isurl(pkg.repo.source)
804+
# Preserve whether the original source was an absolute path - needed later to decide how to store the path
805+
original_source_was_absolute = is_local_path && isabspath(pkg.repo.source)
806+
804807
if is_local_path || pkg.name !== nothing
805-
dev_path = is_local_path ? pkg.repo.source : devpath(ctx.env, pkg.name, shared)
808+
# Resolve manifest-relative paths to absolute paths for file system operations
809+
dev_path = if is_local_path
810+
isabspath(pkg.repo.source) ? pkg.repo.source :
811+
Pkg.manifest_rel_path(ctx.env, pkg.repo.source)
812+
else
813+
devpath(ctx.env, pkg.name, shared)
814+
end
806815
if pkg.repo.subdir !== nothing
807816
dev_path = joinpath(dev_path, pkg.repo.subdir)
808817
end
@@ -818,7 +827,7 @@ function handle_repo_develop!(ctx::Context, pkg::PackageSpec, shared::Bool)
818827
resolve_projectfile!(pkg, dev_path)
819828
error_if_in_sysimage(pkg)
820829
if is_local_path
821-
pkg.path = isabspath(dev_path) ? dev_path : relative_project_path(ctx.env.manifest_file, dev_path)
830+
pkg.path = original_source_was_absolute ? dev_path : relative_project_path(ctx.env.manifest_file, dev_path)
822831
else
823832
pkg.path = shared ? dev_path : relative_project_path(ctx.env.manifest_file, dev_path)
824833
end
@@ -847,7 +856,11 @@ function handle_repo_develop!(ctx::Context, pkg::PackageSpec, shared::Bool)
847856
cloned = false
848857
package_path = pkg.repo.subdir === nothing ? repo_path : joinpath(repo_path, pkg.repo.subdir)
849858
if !has_name(pkg)
850-
LibGit2.close(GitTools.ensure_clone(ctx.io, repo_path, pkg.repo.source))
859+
# Resolve manifest-relative path to absolute before passing to git
860+
repo_source_resolved = !isurl(pkg.repo.source) && !isabspath(pkg.repo.source) ?
861+
Pkg.manifest_rel_path(ctx.env, pkg.repo.source) :
862+
pkg.repo.source
863+
LibGit2.close(GitTools.ensure_clone(ctx.io, repo_path, repo_source_resolved))
851864
cloned = true
852865
resolve_projectfile!(pkg, package_path)
853866
end
@@ -870,7 +883,11 @@ function handle_repo_develop!(ctx::Context, pkg::PackageSpec, shared::Bool)
870883
else
871884
mkpath(dirname(dev_path))
872885
if !cloned
873-
LibGit2.close(GitTools.ensure_clone(ctx.io, dev_path, pkg.repo.source))
886+
# Resolve manifest-relative path to absolute before passing to git
887+
repo_source_resolved = !isurl(pkg.repo.source) && !isabspath(pkg.repo.source) ?
888+
Pkg.manifest_rel_path(ctx.env, pkg.repo.source) :
889+
pkg.repo.source
890+
LibGit2.close(GitTools.ensure_clone(ctx.io, dev_path, repo_source_resolved))
874891
else
875892
mv(repo_path, dev_path)
876893
end
@@ -880,7 +897,13 @@ function handle_repo_develop!(ctx::Context, pkg::PackageSpec, shared::Bool)
880897
resolve_projectfile!(pkg, joinpath(dev_path, pkg.repo.subdir === nothing ? "" : pkg.repo.subdir))
881898
end
882899
error_if_in_sysimage(pkg)
883-
pkg.path = shared ? dev_path : relative_project_path(ctx.env.manifest_file, dev_path)
900+
# When an explicit local path was given, preserve whether it was absolute or relative
901+
# Otherwise, use shared flag to determine if path should be absolute (shared) or relative (local)
902+
if is_local_path
903+
pkg.path = original_source_was_absolute ? dev_path : relative_project_path(ctx.env.manifest_file, dev_path)
904+
else
905+
pkg.path = shared ? dev_path : relative_project_path(ctx.env.manifest_file, dev_path)
906+
end
884907
if pkg.repo.subdir !== nothing
885908
pkg.path = joinpath(pkg.path, pkg.repo.subdir)
886909
end
@@ -948,10 +971,12 @@ function handle_repo_add!(ctx::Context, pkg::PackageSpec)
948971
@assert pkg.repo.source !== nothing
949972

950973
# We now have the source of the package repo, check if it is a local path and if that exists
951-
repo_source = pkg.repo.source
974+
repo_source = !isurl(pkg.repo.source) && !isabspath(pkg.repo.source) ?
975+
normpath(joinpath(dirname(ctx.env.manifest_file), pkg.repo.source)) :
976+
pkg.repo.source
952977
if !isurl(pkg.repo.source)
953-
if isdir(pkg.repo.source)
954-
git_path = joinpath(pkg.repo.source, ".git")
978+
if isdir(repo_source)
979+
git_path = joinpath(repo_source, ".git")
955980
if isfile(git_path)
956981
# Git submodule: .git is a file containing path to actual git directory
957982
git_ref_content = readline(git_path)
@@ -961,22 +986,25 @@ function handle_repo_add!(ctx::Context, pkg::PackageSpec)
961986
git_info_path = git_path
962987
end
963988
if !isdir(git_info_path)
964-
msg = "Did not find a git repository at `$(pkg.repo.source)`"
965-
if isfile(joinpath(pkg.repo.source, "Project.toml")) || isfile(joinpath(pkg.repo.source, "JuliaProject.toml"))
989+
msg = "Did not find a git repository at `$(repo_source)`"
990+
if isfile(joinpath(repo_source, "Project.toml")) || isfile(joinpath(repo_source, "JuliaProject.toml"))
966991
msg *= ", perhaps you meant `Pkg.develop`?"
967992
end
968993
pkgerror(msg)
969994
end
970-
LibGit2.with(GitTools.check_valid_HEAD, LibGit2.GitRepo(pkg.repo.source)) # check for valid git HEAD
971-
LibGit2.with(LibGit2.GitRepo(pkg.repo.source)) do repo
995+
LibGit2.with(GitTools.check_valid_HEAD, LibGit2.GitRepo(repo_source)) # check for valid git HEAD
996+
LibGit2.with(LibGit2.GitRepo(repo_source)) do repo
972997
if LibGit2.isdirty(repo)
973-
@warn "The repository at `$(pkg.repo.source)` has uncommitted changes. Consider using `Pkg.develop` instead of `Pkg.add` if you want to work with the current state of the repository."
998+
@warn "The repository at `$(repo_source)` has uncommitted changes. Consider using `Pkg.develop` instead of `Pkg.add` if you want to work with the current state of the repository."
974999
end
9751000
end
976-
pkg.repo.source = isabspath(pkg.repo.source) ? safe_realpath(pkg.repo.source) : relative_project_path(ctx.env.manifest_file, pkg.repo.source)
977-
repo_source = normpath(joinpath(dirname(ctx.env.manifest_file), pkg.repo.source))
1001+
# Store the path: use the original path format (absolute vs relative) as the user provided
1002+
# Canonicalize repo_source for consistent hashing in cache paths
1003+
repo_source = safe_realpath(repo_source)
1004+
pkg.repo.source = isabspath(pkg.repo.source) ? repo_source : relative_project_path(ctx.env.manifest_file, repo_source)
9781005
else
979-
pkgerror("Path `$(pkg.repo.source)` does not exist.")
1006+
# For error messages, show the absolute path which is more informative than manifest-relative
1007+
pkgerror("Path `$(repo_source)` does not exist.")
9801008
end
9811009
end
9821010

src/utils.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,7 @@ function discover_repo(path::AbstractString)
212212
end
213213
return
214214
end
215+
216+
# Resolve a manifest-relative path to an absolute path
217+
# Note: Despite the name "manifest_rel_path", this resolves relative to the manifest file
218+
manifest_rel_path(env, path::String) = normpath(joinpath(dirname(env.manifest_file), path))

test/new.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1496,7 +1496,7 @@ end
14961496
cd_tempdir() do dir
14971497
# adding a nonexistent directory
14981498
@test_throws PkgError(
1499-
"Path `$(normpath("some/really/random/Dir"))` does not exist."
1499+
"Path `$(abspath("some/really/random/Dir"))` does not exist."
15001500
) Pkg.pkg"add some/really/random/Dir"
15011501
# warn if not explicit about adding directory
15021502
mkdir("Example")

test/pkg.jl

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,4 +1182,56 @@ end
11821182
end
11831183
end
11841184

1185+
# issue #2291: relative paths in manifests should be resolved relative to manifest location
1186+
@testset "relative path resolution from different directories (issue #2291)" begin
1187+
mktempdir() do dir
1188+
# Create a local package with a git repo
1189+
pkg_path = joinpath(dir, "LocalPackage")
1190+
mkpath(joinpath(pkg_path, "src"))
1191+
write(
1192+
joinpath(pkg_path, "Project.toml"), """
1193+
name = "LocalPackage"
1194+
uuid = "00000000-0000-0000-0000-000000000001"
1195+
version = "0.1.0"
1196+
"""
1197+
)
1198+
write(
1199+
joinpath(pkg_path, "src", "LocalPackage.jl"), """
1200+
module LocalPackage
1201+
greet() = "Hello from LocalPackage!"
1202+
end
1203+
"""
1204+
)
1205+
1206+
# Initialize git repo
1207+
LibGit2.with(LibGit2.init(pkg_path)) do repo
1208+
LibGit2.add!(repo, "*")
1209+
LibGit2.commit(repo, "Initial commit"; author = TEST_SIG, committer = TEST_SIG)
1210+
end
1211+
1212+
# Create a project in a subdirectory and add the package with relative path
1213+
project_path = joinpath(dir, "project")
1214+
mkpath(project_path)
1215+
cd(project_path) do
1216+
Pkg.activate(".")
1217+
Pkg.add(Pkg.PackageSpec(path = "../LocalPackage"))
1218+
1219+
# Verify the package was added with relative path
1220+
manifest = read_manifest(joinpath(project_path, "Manifest.toml"))
1221+
pkg_entry = manifest[UUID("00000000-0000-0000-0000-000000000001")]
1222+
@test pkg_entry.repo.source == "../LocalPackage"
1223+
end
1224+
1225+
# Now change to parent directory and try to update - this should work
1226+
cd(dir) do
1227+
Pkg.activate("project")
1228+
Pkg.update() # This should not fail
1229+
# Check the package is installed by looking it up in dependencies
1230+
pkg_info = Pkg.dependencies()[UUID("00000000-0000-0000-0000-000000000001")]
1231+
@test pkg_info.name == "LocalPackage"
1232+
@test isinstalled(PackageSpec(uuid = UUID("00000000-0000-0000-0000-000000000001"), name = "LocalPackage"))
1233+
end
1234+
end
1235+
end
1236+
11851237
end # module

0 commit comments

Comments
 (0)