Skip to content

Commit 0284492

Browse files
committed
feat(foreign-cc): derive runtime library search directories
Add opt-in runtime library search directory derivation for foreign_cc outputs. This lets foreign-built binaries and shared libraries resolve shared libraries from `deps`, `dynamic_deps`, and this rule's own declared shared-library outputs without relying on loader environment variables such as `LD_LIBRARY_PATH`. Public attrs: - `runtime_library_search_directories` - `additional_dynamic_runtime_library_search_origins` - `additional_executable_runtime_library_search_origins` Example: ```python configure_make( name = "python", out_binaries = ["python3.10"], out_shared_libs = ["libpython3.10.so"], runtime_library_search_directories = "enabled", additional_dynamic_runtime_library_search_origins = [ "lib/python3.10/lib-dynload", ], deps = [":openssl"], ) ``` `runtime_library_search_directories` accepts `auto`, `enabled`, and `disabled`. The global build setting defaults to `disabled`, so existing targets keep the previous behavior unless a target opts in or the build setting is enabled. Default origins are derived from declared output `File.short_path` values. Shared-library link actions use declared shared-library output directories. Executable link actions use declared binary output directories. Additional origins are install-tree-relative paths joined under this rule's INSTALLDIR. The implementation derives dependency origins from `LibraryToLink` dynamic libraries and adds Bazel `_solib` sibling search paths for solib symlink layouts. Runtime search derivation is skipped for Windows C++ toolchains. Wire the runtime search path flags as common implementation flags. However, support is limited to `cmake`, `configure_make`, `make` and `meson`. `Ninja` and `boost` support is currently disabled. Add unit coverage for enablement, default origins, additional origins, self-output search paths, `_solib` paths, deduplication, and global build-setting resolution. Add integration coverage for runtime dependency chains and self-contained output bundles.
1 parent f60a9bf commit 0284492

39 files changed

Lines changed: 2750 additions & 28 deletions

examples/integration_tests/runtime_library_search_directories/BUILD.bazel

Lines changed: 480 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
cmake_minimum_required(VERSION 3.15)
2+
3+
project(runtime_search C)
4+
5+
set(RUNTIME_CHAIN_COMPONENT "" CACHE STRING "Component to build: leaf, middle, or app")
6+
set(RUNTIME_TEST_FAMILY "cmake" CACHE STRING "Rule family marker for the runtime test")
7+
8+
if(APPLE)
9+
set(CMAKE_INSTALL_NAME_DIR "@rpath")
10+
set(CMAKE_MACOSX_RPATH ON)
11+
endif()
12+
13+
function(runtime_chain_compile_definitions target)
14+
target_compile_definitions(
15+
${target}
16+
PRIVATE
17+
RUNTIME_TEST_FAMILY="${RUNTIME_TEST_FAMILY}"
18+
)
19+
target_include_directories(${target} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src")
20+
endfunction()
21+
22+
if(RUNTIME_CHAIN_COMPONENT STREQUAL "leaf")
23+
add_library(leaf SHARED src/leaf.c)
24+
runtime_chain_compile_definitions(leaf)
25+
install(TARGETS leaf LIBRARY DESTINATION lib)
26+
elseif(RUNTIME_CHAIN_COMPONENT STREQUAL "middle")
27+
find_library(RUNTIME_CHAIN_LEAF_LIBRARY NAMES leaf REQUIRED)
28+
add_library(middle SHARED src/middle.c)
29+
runtime_chain_compile_definitions(middle)
30+
target_link_libraries(middle PRIVATE "${RUNTIME_CHAIN_LEAF_LIBRARY}")
31+
install(TARGETS middle LIBRARY DESTINATION lib)
32+
elseif(RUNTIME_CHAIN_COMPONENT STREQUAL "app")
33+
find_library(RUNTIME_CHAIN_MIDDLE_LIBRARY NAMES middle REQUIRED)
34+
add_executable(runtime_app src/app.c)
35+
runtime_chain_compile_definitions(runtime_app)
36+
target_link_libraries(runtime_app PRIVATE "${RUNTIME_CHAIN_MIDDLE_LIBRARY}")
37+
install(TARGETS runtime_app RUNTIME DESTINATION bin)
38+
elseif(RUNTIME_CHAIN_COMPONENT STREQUAL "bundle")
39+
add_library(leaf SHARED src/leaf.c)
40+
runtime_chain_compile_definitions(leaf)
41+
add_library(middle SHARED src/middle.c)
42+
runtime_chain_compile_definitions(middle)
43+
target_link_libraries(middle PRIVATE leaf)
44+
add_executable(runtime_app src/app.c)
45+
runtime_chain_compile_definitions(runtime_app)
46+
target_link_libraries(runtime_app PRIVATE middle)
47+
install(TARGETS leaf middle runtime_app LIBRARY DESTINATION lib RUNTIME DESTINATION bin)
48+
else()
49+
message(FATAL_ERROR "RUNTIME_CHAIN_COMPONENT must be one of: leaf, middle, app, bundle")
50+
endif()
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
PREFIX ?= $(INSTALL_PREFIX)
2+
PREFIX ?= install
3+
RUNTIME_TEST_FAMILY ?= make
4+
SRC_DIR ?= src
5+
6+
.PHONY: all clean test
7+
8+
all: app
9+
10+
clean:
11+
12+
test: all
13+
14+
include runtime_chain_rules.mk
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
PREFIX := @prefix@
2+
RUNTIME_TEST_FAMILY := @family@
3+
SRC_DIR := @srcdir@/src
4+
CC := @cc@
5+
CFLAGS := @cflags@
6+
CPPFLAGS := @cppflags@
7+
LDFLAGS := @ldflags@
8+
9+
.PHONY: all clean test
10+
11+
all: app
12+
13+
clean:
14+
15+
test: all
16+
17+
include @srcdir@/runtime_chain_rules.mk
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Runtime library search directories
2+
3+
This package tests that foreign_cc can add runtime loader search paths for
4+
libraries produced by other foreign_cc targets and for libraries produced by the
5+
same foreign_cc target.
6+
7+
The tests are intentionally small. They do not try to cover every possible
8+
native or foreign_cc provider shape; that broader matrix lives in
9+
`examples/integration_tests/transitive_matrix`. This package focuses on whether
10+
the exported `cmake`, `make`, `configure_make`, and `meson` rules pass
11+
the right linker flags to the upstream build system when
12+
`runtime_library_search_directories = "enabled"` is set.
13+
14+
On Linux, the resulting binary and shared libraries use ELF `RUNPATH` or
15+
`RPATH`. On macOS, they use rpath install names.
16+
17+
## Runtime chain
18+
19+
All rule families build the same small C dependency chain:
20+
21+
```text
22+
runtime_app -> libmiddle -> libleaf
23+
```
24+
25+
`libleaf` returns a fixed marker string. `libmiddle` calls `libleaf` and prefixes
26+
the marker with the rule family. `runtime_app` calls `libmiddle`, prints the
27+
result, and fails if the result does not match the expected value.
28+
29+
For example, the CMake dependency-chain test expects:
30+
31+
```text
32+
cmake: expected libmiddle loaded through rpath -> expected libleaf loaded through rpath
33+
```
34+
35+
The runtime test runner clears `LD_LIBRARY_PATH` and `DYLD_LIBRARY_PATH` before
36+
running the binary. A separate runpath test runner inspects the produced binary
37+
and shared libraries so a fallback runtime path cannot hide a missing expected
38+
entry.
39+
40+
## Test shapes
41+
42+
Each supported rule family has two test shapes.
43+
44+
| Test shape | Example target | What it proves |
45+
| ---------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
46+
| Dependency chain | `cmake_test` | A foreign_cc binary target can find a shared library from its direct dependency, and that shared library can find a shared library from its own dependency. |
47+
| App bundle | `cmake_app_bundle_test` | A single foreign_cc target that produces both `bin/runtime_app` and libraries under `lib/` can make those outputs find each other. |
48+
49+
The same shapes are repeated for:
50+
51+
| Rule family | Dependency-chain test | App-bundle test |
52+
| ---------------- | ----------------------- | ---------------------------------- |
53+
| `cmake` | `cmake_test` | `cmake_app_bundle_test` |
54+
| `make` | `make_test` | `make_app_bundle_test` |
55+
| `configure_make` | `configure_make_test` | `configure_make_app_bundle_test` |
56+
| `meson` | `meson_test` | `meson_app_bundle_test` |
57+
58+
## Dependency-chain tests
59+
60+
The dependency-chain tests build each component as a separate foreign_cc target:
61+
62+
```text
63+
<family>_leaf -> installs libleaf under lib/
64+
<family>_middle -> installs libmiddle under lib/, depends on <family>_leaf
65+
<family>_app -> installs runtime_app under bin/, depends on <family>_middle
66+
```
67+
68+
The important behavior is that runtime search directories are derived from the
69+
foreign_cc dependency graph:
70+
71+
- `runtime_app` needs a runtime search path to find `libmiddle`.
72+
- `libmiddle` needs a runtime search path to find `libleaf`.
73+
- The test binary must run without `LD_LIBRARY_PATH` or `DYLD_LIBRARY_PATH`.
74+
- The middle shared library must record self, install-tree-to-solib, and solib
75+
sibling runtime search paths.
76+
77+
## App-bundle tests
78+
79+
The app-bundle tests build `runtime_app`, `libmiddle`, and `libleaf` from one
80+
foreign_cc target:
81+
82+
```text
83+
<family>_app_bundle -> installs runtime_app under bin/
84+
-> installs libmiddle and libleaf under lib/
85+
```
86+
87+
These tests verify the same-target app bundle behavior:
88+
89+
- a binary installed under `bin/` can find libraries installed under `lib/`
90+
- a shared library installed under `lib/` can find sibling libraries in `lib/`
91+
- the app and middle shared library record the expected same-install-tree
92+
runtime search paths.
93+
94+
This is useful for upstream projects that produce an executable and companion
95+
shared libraries in the same install tree, but do not set their own relative
96+
runtime search paths.
97+
98+
## Fixture files
99+
100+
| File | Purpose |
101+
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
102+
| `CMakeLists.txt` | Builds the chain for the `cmake` rule. |
103+
| `Makefile` | Builds the chain directly for the `make` rule. |
104+
| `configure` and `Makefile.in` | Provide a minimal configure-style wrapper for the `configure_make` rule. |
105+
| `runtime_chain_rules.mk` | Shared Makefile logic used by `make` and `configure_make`. |
106+
| `src/` | C sources for `runtime_app`, `libmiddle`, and `libleaf`. |
107+
| `runtime_search_paths_test.sh` | Shared shell test runner that checks recorded runtime search paths on produced binaries and shared libraries. |
108+
| `runtime_search_test.sh` | Shared shell test runner that resolves the Bazel runfile binary, clears ambient library search variables, and checks the printed marker. |
109+
110+
## Run
111+
112+
Run this package from the examples module:
113+
114+
```bash
115+
cd examples
116+
bazel test //integration_tests/runtime_library_search_directories:tests --test_output=errors
117+
```
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
prefix=""
6+
family="configure_make"
7+
8+
for arg in "$@"; do
9+
case "$arg" in
10+
--prefix=*)
11+
prefix="${arg#--prefix=}"
12+
;;
13+
--family=*)
14+
family="${arg#--family=}"
15+
;;
16+
esac
17+
done
18+
19+
if [[ -z "$prefix" ]]; then
20+
echo "missing required --prefix option" >&2
21+
exit 1
22+
fi
23+
24+
srcdir="$(cd "$(dirname "$0")" && pwd)"
25+
26+
escape_sed_replacement() {
27+
printf '%s' "$1" | sed 's/[&|\\]/\\&/g'
28+
}
29+
30+
sed \
31+
-e "s|@prefix@|$prefix|g" \
32+
-e "s|@family@|$family|g" \
33+
-e "s|@srcdir@|$srcdir|g" \
34+
-e "s|@cc@|$(escape_sed_replacement "${CC:-cc}")|g" \
35+
-e "s|@cflags@|$(escape_sed_replacement "${CFLAGS:-}")|g" \
36+
-e "s|@cppflags@|$(escape_sed_replacement "${CPPFLAGS:-}")|g" \
37+
-e "s|@ldflags@|$(escape_sed_replacement "${LDFLAGS:-}")|g" \
38+
"$srcdir/Makefile.in" > Makefile
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
# Meson rewrites Darwin install names and load commands during `meson install`
6+
# from @rpath references to full absolute paths under Bazel's sandbox install
7+
# directory. This fixture repairs Meson's installed outputs back to @rpath names
8+
# so the test stays focused on foreign_cc runtime search paths.
9+
[[ "$(uname -s)" == "Darwin" ]] || exit 0
10+
11+
libdir="${MESON_INSTALL_DESTDIR_PREFIX}/lib"
12+
bindir="${MESON_INSTALL_DESTDIR_PREFIX}/bin"
13+
14+
if [[ -f "$libdir/libleaf.dylib" ]]; then
15+
install_name_tool -id @rpath/libleaf.dylib "$libdir/libleaf.dylib"
16+
fi
17+
18+
if [[ -f "$libdir/libmiddle.dylib" ]]; then
19+
install_name_tool -id @rpath/libmiddle.dylib "$libdir/libmiddle.dylib"
20+
21+
leaf_ref="$(otool -L "$libdir/libmiddle.dylib" | awk '/libleaf[.]dylib/ { print $1; exit }')"
22+
if [[ -n "$leaf_ref" && "$leaf_ref" != "@rpath/libleaf.dylib" ]]; then
23+
install_name_tool -change "$leaf_ref" @rpath/libleaf.dylib "$libdir/libmiddle.dylib"
24+
fi
25+
fi
26+
27+
if [[ -f "$bindir/runtime_app" ]]; then
28+
middle_ref="$(otool -L "$bindir/runtime_app" | awk '/libmiddle[.]dylib/ { print $1; exit }')"
29+
if [[ -n "$middle_ref" && "$middle_ref" != "@rpath/libmiddle.dylib" ]]; then
30+
install_name_tool -change "$middle_ref" @rpath/libmiddle.dylib "$bindir/runtime_app"
31+
fi
32+
fi
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
project('runtime_search', 'c')
2+
3+
runtime_chain_component = get_option('runtime_chain_component')
4+
runtime_test_family = get_option('runtime_test_family')
5+
shared_ldflags = get_option('shared_ldflags')
6+
c = meson.get_compiler('c')
7+
8+
dep_lib_dirs = run_command(
9+
'sh',
10+
'-c',
11+
'if [ -d "$EXT_BUILD_DEPS" ]; then find "$EXT_BUILD_DEPS" -type d -name lib -print | paste -sd: -; fi',
12+
check: true,
13+
).stdout().strip()
14+
dep_lib_dirs = dep_lib_dirs == '' ? [] : dep_lib_dirs.split(':')
15+
16+
compile_args = [
17+
'-DRUNTIME_TEST_FAMILY="@0@"'.format(runtime_test_family),
18+
]
19+
include_dirs = include_directories('src')
20+
install_lib_dir = join_paths(get_option('prefix'), 'lib')
21+
install_bin_dir = join_paths(get_option('prefix'), 'bin')
22+
23+
# Meson rewrites Darwin install names to install paths during install. This
24+
# fixture repairs them so the test remains focused on runtime search paths.
25+
if host_machine.system() == 'darwin'
26+
meson.add_install_script('fix_darwin_install_names.sh')
27+
endif
28+
29+
if runtime_chain_component == 'leaf'
30+
shared_library(
31+
'leaf',
32+
'src/leaf.c',
33+
c_args: compile_args,
34+
include_directories: include_dirs,
35+
install: true,
36+
install_dir: install_lib_dir,
37+
link_args: shared_ldflags,
38+
)
39+
elif runtime_chain_component == 'middle'
40+
leaf_dep = c.find_library('leaf', dirs: dep_lib_dirs, required: true)
41+
shared_library(
42+
'middle',
43+
'src/middle.c',
44+
c_args: compile_args,
45+
dependencies: leaf_dep,
46+
include_directories: include_dirs,
47+
install: true,
48+
install_dir: install_lib_dir,
49+
link_args: shared_ldflags,
50+
)
51+
elif runtime_chain_component == 'app'
52+
middle_dep = c.find_library('middle', dirs: dep_lib_dirs, required: true)
53+
executable(
54+
'runtime_app',
55+
'src/app.c',
56+
c_args: compile_args,
57+
dependencies: middle_dep,
58+
include_directories: include_dirs,
59+
install: true,
60+
install_dir: install_bin_dir,
61+
)
62+
elif runtime_chain_component == 'bundle'
63+
leaf_lib = shared_library(
64+
'leaf',
65+
'src/leaf.c',
66+
c_args: compile_args,
67+
include_directories: include_dirs,
68+
install: true,
69+
install_dir: install_lib_dir,
70+
link_args: shared_ldflags,
71+
)
72+
middle_lib = shared_library(
73+
'middle',
74+
'src/middle.c',
75+
c_args: compile_args,
76+
include_directories: include_dirs,
77+
install: true,
78+
install_dir: install_lib_dir,
79+
link_args: shared_ldflags,
80+
link_with: leaf_lib,
81+
)
82+
executable(
83+
'runtime_app',
84+
'src/app.c',
85+
c_args: compile_args,
86+
include_directories: include_dirs,
87+
install: true,
88+
install_dir: install_bin_dir,
89+
link_with: middle_lib,
90+
)
91+
else
92+
error('runtime_chain_component must be one of: leaf, middle, app, bundle')
93+
endif
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
option('runtime_chain_component', type: 'string', value: '')
2+
option('runtime_test_family', type: 'string', value: 'meson')
3+
option('shared_ldflags', type: 'array', value: [])

0 commit comments

Comments
 (0)