Skip to content

Commit ee05c44

Browse files
Fix patchelf concurrency issues. Show subprocess output in errors (#1410)
* Show subprocess output in error message when run_with_io fails * Fix concurrency issues in check_dynamic_linkage Remove nested Threads.@threads over libraries since the outer loop over files already provides parallelism. Nested threading caused: 1. Race conditions with patchelf/install_name_tool on the same file 2. ConcurrencyViolationError in OutputCollectors when multiple sandbox operations ran in parallel * Revert "Fix concurrency issues in check_dynamic_linkage" This reverts commit 04d4f81. * rm OutputCollectors (unused) * Add Per-file locks for patchelf operations * update to 1.12 * go back to unbuffer julia-1.7 * Extend patchelf lock to cover file reads Wrap the entire read-modify-verify cycle in with_patchelf_lock() for Linux/BSD ELF operations. This prevents concurrent ObjectFile reads from failing with EOFError when another thread is running patchelf on the same file. Split update_linkage into _update_linkage_macho and _update_linkage_elf, and ensure_soname into _ensure_soname_macho and _ensure_soname_elf for cleaner platform separation. * Fix platform detection: check macOS before BSD Sys.isbsd(platform) returns true for macOS since Darwin is BSD-based, but macOS uses install_name_tool not patchelf. Check Sys.isapple first. * Reduce needless code duplication * Reduce needless diff --------- Co-authored-by: Mosè Giordano <mose@gnu.org>
1 parent f16ad36 commit ee05c44

File tree

8 files changed

+135
-83
lines changed

8 files changed

+135
-83
lines changed

.github/workflows/documentation.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
- uses: actions/checkout@v6
2121
- uses: julia-actions/setup-julia@latest
2222
with:
23-
version: "1.7"
23+
version: "1.12"
2424
- uses: julia-actions/cache@v2
2525
with:
2626
cache-registries: "true"

Project.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
1818
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
1919
LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36"
2020
ObjectFile = "d8793406-e978-5875-9003-1fc021f44a92"
21-
OutputCollectors = "6c11c7d4-943b-4e2b-80de-f2cfc2930a8c"
2221
Patchelf_jll = "f2cf89d6-2bfd-5c44-bd2c-068eea195c0c"
2322
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
2423
PkgLicenses = "fc669557-7ec9-5e45-bca9-462afbc28879"
@@ -45,7 +44,6 @@ JLLWrappers = "1.2.0"
4544
JSON = "0.21, 1"
4645
LoggingExtras = "0.4, 1"
4746
ObjectFile = "0.4.3"
48-
OutputCollectors = "0.1"
4947
Patchelf_jll = "0.14.3"
5048
PkgLicenses = "0.2"
5149
Registrator = "1.1"

docs/src/index.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ The purpose of the [`BinaryBuilder.jl`](https://github.com/JuliaPackaging/Binary
44

55
Note that at this time, BinaryBuilder itself runs on Linux `x86_64` and macOS `x86_64` systems only, with Windows support under active development. On macOS and Windows, you must have `docker` installed as the backing virtualization engine. Note that Docker Desktop is the recommended version; if you have Docker Machine installed it may not work correctly or may need additional configuration.
66

7-
!!! warn
8-
9-
This package currently requires Julia v1.7. Contribute to [JuliaPackaging/JLLPrefixes.jl#6](https://github.com/JuliaPackaging/JLLPrefixes.jl/issues/6) if you care about supporting newer versions of Julia.
10-
117
## Project flow
128

139
Suppose that you have a Julia package `Foo.jl` which wants to use a compiled `libfoo` shared library. As your first step in writing `Foo.jl`, you may compile `libfoo` locally on your own machine with your system compiler, then using `Libdl.dlopen()` to open the library, and `ccall()` to call into the exported functions. Once you have written your C bindings in Julia, you will naturally desire to share the fruits of your labor with the rest of the world, and this is where `BinaryBuilder` can help you. Not only will `BinaryBuilder` aid you in constructing compiled versions of all your dependencies, but it will also build a wrapper Julia package (referred to as a [JLL package](jll.md)) to aid in installation, versioning, and build product localization.

src/Auditor.jl

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,46 @@ const AUDITOR_SANDBOX_LOCK = ReentrantLock()
1616
# Logging is reportedly not thread-safe but guarding it with locks should help.
1717
const AUDITOR_LOGGING_LOCK = ReentrantLock()
1818

19+
# Per-file locks for patchelf operations. Multiple threads may try to modify
20+
# the same binary file (e.g., relinking different libraries), so we need to
21+
# serialize access per file.
22+
const PATCHELF_FILE_LOCKS = Dict{String,ReentrantLock}()
23+
const PATCHELF_FILE_LOCKS_LOCK = ReentrantLock()
24+
25+
"""
26+
with_patchelf_lock(f, path::AbstractString)
27+
28+
Execute `f()` while holding a lock specific to the file at `path`.
29+
This prevents concurrent patchelf operations on the same file.
30+
"""
31+
function with_patchelf_lock(f, path::AbstractString)
32+
# Normalize the path to ensure consistent locking
33+
path = realpath(path)
34+
35+
# Get or create the lock for this file
36+
file_lock = Base.@lock PATCHELF_FILE_LOCKS_LOCK begin
37+
get!(PATCHELF_FILE_LOCKS, path) do
38+
ReentrantLock()
39+
end
40+
end
41+
42+
# Execute with the file-specific lock held
43+
Base.@lock file_lock f()
44+
end
45+
1946
# Helper function to run a command and print to `io` its invocation and full
20-
# output (mimim what the sandbox does normally, but outside of it).
47+
# output (mimic what the sandbox does normally, but outside of it).
2148
function run_with_io(io::IO, cmd::Cmd; wait::Bool=true)
2249
println(io, "---> $(join(cmd.exec, " "))")
23-
run(pipeline(cmd; stdout=io, stderr=io); wait)
50+
output = IOBuffer()
51+
proc = run(pipeline(cmd; stdout=output, stderr=output); wait=false)
52+
Base.wait(proc)
53+
out_str = String(take!(output))
54+
print(io, out_str)
55+
if wait && !success(proc)
56+
error("Command failed: $(cmd)\nOutput:\n$(out_str)")
57+
end
58+
return proc
2459
end
2560

2661
include("auditor/instruction_set.jl")

src/BinaryBuilder.jl

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const runshell = BinaryBuilderBase.runshell
3434
include("Auditor.jl")
3535
include("Wizard.jl")
3636

37-
using OutputCollectors, BinaryBuilderBase, .Auditor, .Wizard
37+
using BinaryBuilderBase, .Auditor, .Wizard
3838

3939
# Autocomplete BinaryBuilder.run_wizard
4040
const run_wizard = Wizard.run_wizard
@@ -44,13 +44,6 @@ include("Declarative.jl")
4444
include("Logging.jl")
4545

4646
function __init__()
47-
if Base.thisminor(VERSION) >= v"1.8" && get(ENV, "JULIA_REGISTRYCI_AUTOMERGE", "false") != "true"
48-
error("""
49-
BinaryBuilder supports only Julia v1.7.
50-
Contribute to JuliaPackaging/JLLPrefixes.jl#6 (<https://github.com/JuliaPackaging/JLLPrefixes.jl/issues/6>)
51-
if you care about supporting newer versions of Julia.
52-
""")
53-
end
5447
# If we're running on Azure, enable azure logging:
5548
if !isempty(get(ENV, "AZP_TOKEN", ""))
5649
enable_azure_logging()

src/Wizard.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module Wizard
22

3-
using BinaryBuilderBase, OutputCollectors, ..Auditor
3+
using BinaryBuilderBase, ..Auditor
44
using Random
55
using GitHub, LibGit2, Pkg, Sockets, ObjectFile
66
import GitHub: gh_get_json, DEFAULT_API

src/auditor/dynamic_linkage.jl

Lines changed: 90 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,9 @@ function relink_to_rpath(prefix::Prefix, platform::AbstractPlatform, path::Abstr
392392
relink_cmd = `$install_name_tool -change $(old_libpath) @rpath/$(libname) $(rel_path)`
393393
@lock AUDITOR_SANDBOX_LOCK run(ur, relink_cmd, io; verbose=verbose)
394394
elseif Sys.islinux(platform) || Sys.isbsd(platform)
395-
run_with_io(io, `$(patchelf()) $(patchelf_flags(platform)) --replace-needed $(old_libpath) $(libname) $(path)`)
395+
with_patchelf_lock(path) do
396+
run_with_io(io, `$(patchelf()) $(patchelf_flags(platform)) --replace-needed $(old_libpath) $(libname) $(path)`)
397+
end
396398
end
397399
end
398400
end
@@ -448,57 +450,32 @@ function update_linkage(prefix::Prefix, platform::AbstractPlatform, path::Abstra
448450
return
449451
end
450452

453+
# macOS uses install_name_tool
454+
if Sys.isapple(platform)
455+
return _update_linkage_macho(prefix, platform, path, old_libpath, new_libpath; verbose, subdir)
456+
end
457+
458+
# For Linux/FreeBSD, wrap the entire read-modify-write cycle in the file lock
459+
# to prevent concurrent access while reading rpaths and running patchelf
460+
return with_patchelf_lock(path) do
461+
_update_linkage_elf(prefix, platform, path, old_libpath, new_libpath; verbose, subdir)
462+
end
463+
end
464+
465+
function _update_linkage_macho(prefix::Prefix, platform::AbstractPlatform, path::AbstractString,
466+
old_libpath, new_libpath; verbose::Bool=false, subdir::AbstractString="")
451467
ur = preferred_runner()(prefix.path; cwd="/workspace/", platform=platform)
452468
rel_path = relpath(path, prefix.path)
453-
454-
normalize_rpath = rp -> rp
455-
add_rpath = x -> ``
456-
relink = (x, y) -> ``
457469
install_name_tool = "/opt/bin/$(triplet(ur.platform))/install_name_tool"
458-
if Sys.isapple(platform)
459-
normalize_rpath = rp -> begin
460-
if !startswith(rp, "@loader_path")
461-
return "@loader_path/$(rp)"
462-
end
463-
return rp
464-
end
465-
add_rpath = rp -> `$install_name_tool -add_rpath $(rp) $(rel_path)`
466-
relink = (op, np) -> `$install_name_tool -change $(op) $(np) $(rel_path)`
467-
elseif Sys.islinux(platform) || Sys.isbsd(platform)
468-
normalize_rpath = rp -> begin
469-
if rp == "."
470-
return "\$ORIGIN"
471-
end
472-
if startswith(rp, ".") || !startswith(rp, "/")
473-
# Relative paths starting with `.`, or anything which isn't an absolute
474-
# path. It may also be a relative path without the leading `./`
475-
return "\$ORIGIN/$(rp)"
476-
end
477-
return rp
478-
end
479-
current_rpaths = [r for r in _rpaths(path) if !isempty(r)]
480-
add_rpath = rp -> begin
481-
# Join together RPaths to set new one
482-
rpaths = unique(vcat(current_rpaths, rp))
483-
484-
# I don't like strings ending in '/.', like '$ORIGIN/.'. I don't think
485-
# it semantically makes a difference, but why not be correct AND beautiful?
486-
chomp_slashdot = path -> begin
487-
if length(path) > 2 && path[end-1:end] == "/."
488-
return path[1:end-2]
489-
end
490-
return path
491-
end
492-
rpaths = chomp_slashdot.(rpaths)
493-
# Remove paths starting with `/workspace`: they will not work outisde of the
494-
# build environment and only create noise when debugging.
495-
filter!(rp -> !startswith(rp, "/workspace"), rpaths)
496470

497-
rpath_str = join(rpaths, ':')
498-
return `$(patchelf()) $(patchelf_flags(platform)) --set-rpath $(rpath_str) $(path)`
471+
normalize_rpath = rp -> begin
472+
if !startswith(rp, "@loader_path")
473+
return "@loader_path/$(rp)"
499474
end
500-
relink = (op, np) -> `$(patchelf()) $(patchelf_flags(platform)) --replace-needed $(op) $(np) $(path)`
475+
return rp
501476
end
477+
add_rpath = rp -> `$install_name_tool -add_rpath $(rp) $(rel_path)`
478+
relink = (op, np) -> `$install_name_tool -change $(op) $(np) $(rel_path)`
502479

503480
# If the relative directory doesn't already exist within the RPATH of this
504481
# binary, then add it in.
@@ -507,36 +484,86 @@ function update_linkage(prefix::Prefix, platform::AbstractPlatform, path::Abstra
507484
libname = basename(old_libpath)
508485
cmd = add_rpath(normalize_rpath(relpath(new_libdir, dirname(path))))
509486
with_logfile(prefix, "update_rpath_$(basename(path))_$(libname).log"; subdir) do io
510-
if Sys.isapple(platform)
511-
@lock AUDITOR_SANDBOX_LOCK run(ur, cmd, io; verbose=verbose)
512-
elseif Sys.islinux(platform) || Sys.isbsd(platform)
513-
run_with_io(io, cmd)
514-
end
487+
@lock AUDITOR_SANDBOX_LOCK run(ur, cmd, io; verbose=verbose)
515488
end
516489
end
517490

518491
# Create a new linkage that uses the RPATH and/or environment variables to find things.
519492
# This allows us to split things up into multiple packages, and as long as the
520493
# libraries that this guy is interested in have been `dlopen()`'ed previously,
521494
# (and have the appropriate SONAME) things should "just work".
522-
if Sys.isapple(platform)
523-
# On MacOS, we need to explicitly add `@rpath/` before our library linkage path.
524-
# Note that this is still overridable through DYLD_FALLBACK_LIBRARY_PATH
525-
new_libpath = joinpath("@rpath", basename(new_libpath))
526-
else
527-
# We just use the basename on all other systems (e.g. Linux). Note that using
528-
# $ORIGIN, while cute, doesn't allow for overrides via LD_LIBRARY_PATH. :[
529-
new_libpath = basename(new_libpath)
530-
end
495+
# On MacOS, we need to explicitly add `@rpath/` before our library linkage path.
496+
# Note that this is still overridable through DYLD_FALLBACK_LIBRARY_PATH
497+
new_libpath = joinpath("@rpath", basename(new_libpath))
531498
cmd = relink(old_libpath, new_libpath)
532499
with_logfile(prefix, "update_linkage_$(basename(path))_$(basename(old_libpath)).log"; subdir) do io
533-
if Sys.isapple(platform)
534-
@lock AUDITOR_SANDBOX_LOCK run(ur, cmd, io; verbose=verbose)
535-
elseif Sys.islinux(platform) || Sys.isbsd(platform)
500+
@lock AUDITOR_SANDBOX_LOCK run(ur, cmd, io; verbose=verbose)
501+
end
502+
503+
return new_libpath
504+
end
505+
506+
# This function is called with the patchelf lock already held
507+
function _update_linkage_elf(prefix::Prefix, platform::AbstractPlatform, path::AbstractString,
508+
old_libpath, new_libpath; verbose::Bool=false, subdir::AbstractString="")
509+
normalize_rpath = rp -> begin
510+
if rp == "."
511+
return "\$ORIGIN"
512+
end
513+
if startswith(rp, ".") || !startswith(rp, "/")
514+
# Relative paths starting with `.`, or anything which isn't an absolute
515+
# path. It may also be a relative path without the leading `./`
516+
return "\$ORIGIN/$(rp)"
517+
end
518+
return rp
519+
end
520+
521+
current_rpaths = [r for r in _rpaths(path) if !isempty(r)]
522+
523+
add_rpath = rp -> begin
524+
# Join together RPaths to set new one
525+
rpaths = unique(vcat(current_rpaths, rp))
526+
# I don't like strings ending in '/.', like '$ORIGIN/.'. I don't think
527+
# it semantically makes a difference, but why not be correct AND beautiful?
528+
chomp_slashdot = path -> begin
529+
if length(path) > 2 && path[end-1:end] == "/."
530+
return path[1:end-2]
531+
end
532+
return path
533+
end
534+
rpaths = chomp_slashdot.(rpaths)
535+
# Remove paths starting with `/workspace`: they will not work outside of the
536+
# build environment and only create noise when debugging.
537+
filter!(rp -> !startswith(rp, "/workspace"), rpaths)
538+
rpath_str = join(rpaths, ':')
539+
return `$(patchelf()) $(patchelf_flags(platform)) --set-rpath $(rpath_str) $(path)`
540+
end
541+
542+
relink = (op, np) -> `$(patchelf()) $(patchelf_flags(platform)) --replace-needed $(op) $(np) $(path)`
543+
544+
# If the relative directory doesn't already exist within the RPATH of this
545+
# binary, then add it in.
546+
new_libdir = abspath(dirname(new_libpath) * "/")
547+
if !(new_libdir in _canonical_rpaths(path))
548+
libname = basename(old_libpath)
549+
cmd = add_rpath(normalize_rpath(relpath(new_libdir, dirname(path))))
550+
with_logfile(prefix, "update_rpath_$(basename(path))_$(libname).log"; subdir) do io
536551
run_with_io(io, cmd)
537552
end
538553
end
539554

555+
# Create a new linkage that uses the RPATH and/or environment variables to find things.
556+
# This allows us to split things up into multiple packages, and as long as the
557+
# libraries that this guy is interested in have been `dlopen()`'ed previously,
558+
# (and have the appropriate SONAME) things should "just work".
559+
# We just use the basename on Linux/BSD. Note that using $ORIGIN, while cute,
560+
# doesn't allow for overrides via LD_LIBRARY_PATH. :[
561+
new_libpath = basename(new_libpath)
562+
cmd = relink(old_libpath, new_libpath)
563+
with_logfile(prefix, "update_linkage_$(basename(path))_$(basename(old_libpath)).log"; subdir) do io
564+
run_with_io(io, cmd)
565+
end
566+
540567
return new_libpath
541568
end
542569

src/auditor/soname_matching.jl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,18 @@ function ensure_soname(prefix::Prefix, path::AbstractString, platform::AbstractP
6565
end
6666

6767
# Otherwise, set the SONAME
68-
# Create a new linkage that looks like @rpath/$lib on OSX,
68+
# Create a new linkage that looks like @rpath/$lib on OSX
6969
retval = with_logfile(prefix, "set_soname_$(basename(rel_path))_$(soname).log"; subdir) do io
7070
if Sys.isapple(platform)
7171
ur = preferred_runner()(prefix.path; cwd="/workspace/", platform=platform)
7272
install_name_tool = "/opt/bin/$(triplet(ur.platform))/install_name_tool"
7373
set_soname_cmd = `$install_name_tool -id $(soname) $(rel_path)`
7474
@lock AUDITOR_SANDBOX_LOCK run(ur, set_soname_cmd, io; verbose=verbose)
7575
elseif Sys.islinux(platform) || Sys.isbsd(platform)
76-
success(run_with_io(io, `$(patchelf()) $(patchelf_flags(platform)) --set-soname $(soname) $(realpath(path))`; wait=false))
76+
# For Linux/FreeBSD, wrap the entire read-modify-verify cycle in the file lock
77+
with_patchelf_lock(path) do
78+
success(run_with_io(io, `$(patchelf()) $(patchelf_flags(platform)) --set-soname $(soname) $(realpath(path))`; wait=false))
79+
end
7780
end
7881
end
7982

0 commit comments

Comments
 (0)