Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/Types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Base.string

using TOML
import ..Pkg, ..Registry
import ..Pkg: GitTools, depots, depots1, logdir, set_readonly, safe_realpath, pkg_server, stdlib_dir, stdlib_path, isurl, stderr_f, RESPECT_SYSIMAGE_VERSIONS, atomic_toml_write, create_cachedir_tag
import ..Pkg: GitTools, depots, depots1, logdir, set_readonly, safe_realpath, pkg_server, stdlib_dir, stdlib_path, isurl, stderr_f, RESPECT_SYSIMAGE_VERSIONS, atomic_toml_write, create_cachedir_tag, normalize_path_for_toml
import Base.BinaryPlatforms: Platform
using ..Pkg.Versions
import FileWatching
Expand Down
8 changes: 4 additions & 4 deletions src/manifest.jl
Original file line number Diff line number Diff line change
Expand Up @@ -329,14 +329,14 @@ function destructure(manifest::Manifest)::Dict
entry!(new_entry, "git-tree-sha1", entry.tree_hash)
entry!(new_entry, "pinned", entry.pinned; default = false)
path = entry.path
if path !== nothing && Sys.iswindows() && !isabspath(path)
path = join(splitpath(path), "/")
if path !== nothing
path = normalize_path_for_toml(path)
end
entry!(new_entry, "path", path)
entry!(new_entry, "entryfile", entry.entryfile)
repo_source = entry.repo.source
if repo_source !== nothing && Sys.iswindows() && !isabspath(repo_source) && !isurl(repo_source)
repo_source = join(splitpath(repo_source), "/")
if repo_source !== nothing && !isurl(repo_source)
repo_source = normalize_path_for_toml(repo_source)
end
entry!(new_entry, "repo-url", repo_source)
entry!(new_entry, "repo-rev", entry.repo.rev)
Expand Down
16 changes: 15 additions & 1 deletion src/project.jl
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,21 @@ function destructure(project::Project)::Dict
entry!("entryfile", project.entryfile)
entry!("deps", merge(project.deps, project._deps_weak))
entry!("weakdeps", project.weakdeps)
entry!("sources", project.sources)

# Normalize paths in sources to use forward slashes on Windows (matching Manifest.toml behavior)
normalized_sources = project.sources
if !isempty(project.sources)
normalized_sources = Dict{String, Dict{String, String}}()
for (name, source) in project.sources
normalized_source = copy(source)
path = get(source, "path", nothing)
if path !== nothing
normalized_source["path"] = normalize_path_for_toml(path)
end
normalized_sources[name] = normalized_source
end
end
entry!("sources", normalized_sources)
entry!("extras", project.extras)
entry!("compat", Dict(name => x.str for (name, x) in project.compat))
entry!("targets", project.targets)
Expand Down
14 changes: 14 additions & 0 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ function pathrepr(path::String)
return "`" * Base.contractuser(path) * "`"
end

"""
normalize_path_for_toml(path::String)

Normalize a path for writing to TOML files (Project.toml/Manifest.toml).
On Windows, converts relative paths to use forward slashes for cross-platform compatibility.
Absolute paths are left unchanged as they are platform-specific by nature.
"""
function normalize_path_for_toml(path::String)
if Sys.iswindows() && !isabspath(path)
return join(splitpath(path), "/")
end
return path
end

function set_readonly(path)
for (root, dirs, files) in walkdir(path)
for file in files
Expand Down
19 changes: 19 additions & 0 deletions test/misc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,25 @@ end
end
end

@testset "normalize_path_for_toml" begin
# Test that relative paths with backslashes are normalized to forward slashes on Windows
# and left unchanged on other platforms
if Sys.iswindows()
@test Pkg.normalize_path_for_toml("foo\\bar\\baz") == "foo/bar/baz"
@test Pkg.normalize_path_for_toml("..\\parent\\dir") == "../parent/dir"
@test Pkg.normalize_path_for_toml(".\\current") == "./current"
# Absolute paths should not be normalized (they're platform-specific)
@test Pkg.normalize_path_for_toml("C:\\absolute\\path") == "C:\\absolute\\path"
@test Pkg.normalize_path_for_toml("\\\\network\\share") == "\\\\network\\share"
else
# On Unix-like systems, paths should be unchanged
@test Pkg.normalize_path_for_toml("foo/bar/baz") == "foo/bar/baz"
@test Pkg.normalize_path_for_toml("../parent/dir") == "../parent/dir"
@test Pkg.normalize_path_for_toml("./current") == "./current"
@test Pkg.normalize_path_for_toml("/absolute/path") == "/absolute/path"
end
end

@test eltype([PackageSpec(a) for a in []]) == PackageSpec

@testset "PackageSpec version default" begin
Expand Down
37 changes: 37 additions & 0 deletions test/sources.jl
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,43 @@ temp_pkg_dir() do project_path
end
end
end

@testset "path normalization in Project.toml [sources]" begin
mktempdir() do tmp
cd(tmp) do
# Create a minimal Project.toml with sources containing a path
write(
"Project.toml",
"""
name = "TestPackage"
uuid = "12345678-1234-1234-1234-123456789abc"

[deps]
LocalPkg = "87654321-4321-4321-4321-cba987654321"

[sources]
LocalPkg = { path = "subdir/LocalPkg" }
"""
)

# Read the project
project = Pkg.Types.read_project("Project.toml")

# Verify the path is read correctly (will have native separators internally)
@test haskey(project.sources, "LocalPkg")
@test haskey(project.sources["LocalPkg"], "path")

# Write it back
Pkg.Types.write_project(project, "Project.toml")

# Read the written file as string and verify forward slashes are used
project_content = read("Project.toml", String)
@test occursin("path = \"subdir/LocalPkg\"", project_content)
# Verify backslashes are NOT in the path (would indicate Windows path wasn't normalized)
@test !occursin("path = \"subdir\\\\LocalPkg\"", project_content)
end
end
end
end

end # module