diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index 833beff62..d53d636d7 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -89,6 +89,16 @@ stardoc( ], ) +stardoc( + name = "runtime_executable_docs", + out = "src/runtime_executable.md", + input = "@rules_foreign_cc//foreign_cc:runtime_executable.bzl", + deps = [ + ":rules_cc_deps", + "@rules_foreign_cc//foreign_cc:runtime_executable", + ], +) + DOCS_TARGETS = [ ":cmake_docs", ":ninja_docs", @@ -97,6 +107,7 @@ DOCS_TARGETS = [ ":meson_docs", ":msbuild_docs", ":providers_docs", + ":runtime_executable_docs", ] build_test( diff --git a/docs/docs.bzl b/docs/docs.bzl index 3690e1dfc..c19537d60 100644 --- a/docs/docs.bzl +++ b/docs/docs.bzl @@ -12,11 +12,13 @@ load( _meson = "meson", _meson_with_requirements = "meson_with_requirements", _ninja = "ninja", + _runtime_executable = "runtime_executable", ) load( "@rules_foreign_cc//foreign_cc:providers.bzl", _ForeignCcArtifactInfo = "ForeignCcArtifactInfo", _ForeignCcDepsInfo = "ForeignCcDepsInfo", + _ForeignCcRuntimeExecutableInfo = "ForeignCcRuntimeExecutableInfo", ) load("@rules_foreign_cc//foreign_cc:repositories.bzl", _rules_foreign_cc_dependencies = "rules_foreign_cc_dependencies") load("@rules_foreign_cc//foreign_cc/built_tools:cmake_build.bzl", _cmake_tool = "cmake_tool") @@ -44,7 +46,9 @@ native_tool_toolchain = _native_tool_toolchain ninja = _ninja ninja_tool = _ninja_tool rules_foreign_cc_dependencies = _rules_foreign_cc_dependencies +runtime_executable = _runtime_executable ForeignCcArtifactInfo = _ForeignCcArtifactInfo ForeignCcDepsInfo = _ForeignCcDepsInfo +ForeignCcRuntimeExecutableInfo = _ForeignCcRuntimeExecutableInfo ToolInfo = _ToolInfo diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 5271bd05b..71dd8e19f 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -4,6 +4,7 @@ - [bzlmod hub-and-spoke](bzlmod_hub.md) - [Examples](bzlmod_examples.md) - [Migrating from WORKSPACE](bzlmod_migration.md) + - [Runtime library search directories](runtime_search.md) - [Rules](rules.md) - [cmake](cmake.md) - [configure_make](configure_make.md) @@ -11,3 +12,4 @@ - [meson](meson.md) - [msbuild](msbuild.md) - [ninja](ninja.md) + - [runtime_executable](runtime_executable.md) diff --git a/docs/src/rules.md b/docs/src/rules.md index 0f32bbc0d..e161e6093 100644 --- a/docs/src/rules.md +++ b/docs/src/rules.md @@ -5,5 +5,6 @@ - [make](./make.md) - [meson](./meson.md) - [ninja](./ninja.md) +- [runtime_executable](./runtime_executable.md) For additional rules/macros/providers, see the [full API in one page](./flatten.md). diff --git a/docs/src/runtime_search.md b/docs/src/runtime_search.md new file mode 100644 index 000000000..fd7dff225 --- /dev/null +++ b/docs/src/runtime_search.md @@ -0,0 +1,221 @@ +# Runtime library search directories + +## What this feature does + +When a `foreign_cc` rule produces shared libraries or executables, those outputs +often depend on *other* shared libraries — from the rule's `deps`, its +`dynamic_deps`, or its own install tree. At run time the dynamic loader has to +find those `.so`/`.dylib` files. By default it can only find them via ambient +environment (`LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH`) or absolute paths baked in +by the upstream build — neither of which survives Bazel's sandbox or a +relocated install tree. + +This feature makes `foreign_cc` derive **runtime library search directories** +(rpaths) and pass them to the upstream build system's link actions. The derived +paths are expressed relative to the loading object using `$ORIGIN` (Linux) or +`@loader_path` (macOS), so the installed outputs locate their shared-library +dependencies within the Bazel build with the loader environment unset. + +In short: **the outputs become self-locating inside Bazel** — no +`LD_LIBRARY_PATH`, no absolute install paths. + +The derived paths are computed in Bazel's runfiles/execroot coordinate space, +and the entries for dependency libraries point at Bazel's layout (e.g. +`external//...` and the `_solib_*` symlink farm). This means the feature +resolves shared-library references **within the Bazel build and its runfiles**, +not in an install tree that has been copied or shipped outside Bazel. A target's +rpath to its *own* install tree (a binary finding its sibling `lib/`) is +`$ORIGIN`-relative and so survives relocation, but its rpaths to `deps` / +`dynamic_deps` libraries assume the Bazel layout. + +## When to use it + +| Situation | Use it? | +| --- | --- | +| You consume the `foreign_cc` output's shared libs / binaries from other Bazel targets and want them to run without `LD_LIBRARY_PATH` | **Yes** | +| The output is a binary that loads shared libs from its own install tree (e.g. `bin/app` → `lib/libfoo.so`) | **Yes** | +| The output depends on `deps` / `dynamic_deps` shared libs and must run inside the Bazel build / runfiles without `LD_LIBRARY_PATH` | **Yes** | +| You copy/ship the install tree *outside* Bazel and expect its `deps` references to keep resolving | No — derived dep paths assume Bazel's layout (see [Caveats](#caveats)) | +| The build produces only static libraries / headers (no shared libs or executables that load them) | Not needed | +| You target Windows C++ toolchains | Not supported (see [Caveats](#caveats)) | + +## How to enable it + +Enablement has two layers: a **per-target attribute** and a **global build +setting**. The per-target attribute is `runtime_library_search_directories` and +defaults to `auto`. + +| Per-target attr value | Effect | +| --- | --- | +| `"auto"` *(default)* | Follow the global build setting | +| `"enabled"` | Force on for this target, regardless of the global setting | +| `"disabled"` | Force off for this target, regardless of the global setting | + +The global build setting is +`@rules_foreign_cc//foreign_cc/settings:runtime_library_search_directories`. It +defaults to `disabled`. + +| Global setting value | Effect on `auto` targets | +| --- | --- | +| `"disabled"` *(default)* | `auto` targets do **not** derive runtime paths | +| `"enabled"` | `auto` targets **do** derive runtime paths | + +The resolved behavior is the combination of the two. The per-target attribute +takes precedence: `"enabled"` and `"disabled"` override the global setting +outright, and only `"auto"` defers to it. + +| Per-target attr | Global setting | Result | +| --- | --- | --- | +| `auto` | `disabled` | Off | +| `auto` | `enabled` | On | +| `enabled` | *(any)* | On | +| `disabled` | *(any)* | Off | + +### Enable for a single target + +```python +load("@rules_foreign_cc//foreign_cc:defs.bzl", "make") + +make( + name = "libfoo", + lib_source = ":srcs", + out_shared_libs = ["libfoo.so"], + # Turn the feature on just for this target. + runtime_library_search_directories = "enabled", + # Required for shared-lib outputs (see "Rule support" below). + shared_ldflags_vars = ["LDFLAGS"], + targets = ["", "install"], +) +``` + +### Enable globally + +Flip the global setting on in your `.bazelrc` so every `auto` target opts in: + +```text +# .bazelrc +build --@rules_foreign_cc//foreign_cc/settings:runtime_library_search_directories=enabled +``` + +Global enablement is a strict Unix-conformance switch: any Unix target that +declares shared-library outputs must then either wire up its shared-link flag +hook (below) or set `runtime_library_search_directories = "disabled"`. + +## Rule support + +| Rule | Supported | Shared-lib hook attr (required for `out_shared_libs`) | +| --- | --- | --- | +| `cmake` | Yes | *(automatic)* | +| `make` | Yes | `shared_ldflags_vars` | +| `configure_make` | Yes | `shared_ldflags_vars` | +| `meson` | Yes | `shared_ldflags_option` | +| `ninja` | No — fails if enabled | — | +| Boost (`boost_build`) | No — fails if enabled | — | +| `msbuild` | No (Windows) | — | + +For `make`, `configure_make`, and `meson`, the derived shared-library linker +flags are handed to the upstream build through a named flag variable/option that +you forward into the actual link command. **When a target declares +`out_shared_libs` and the feature is enabled, the shared-lib hook attr is +required** — otherwise analysis fails, because the shared-library rpaths would +have nowhere to go. `cmake` injects the flags automatically through +`CMAKE_SHARED_LINKER_FLAGS_INIT` / `CMAKE_EXE_LINKER_FLAGS_INIT`, so no hook attr +is needed. + +**Executable rpaths do not need a hook attr.** They flow through each rule's +default executable-linker route (for `make`/`configure_make`, the standard +`LDFLAGS` make variable), so no analysis error is raised for executables. The +`make`/`configure_make` `executable_ldflags_vars` attr is only needed when you +have repurposed `LDFLAGS` for shared libraries and want executable flags routed +to a *different* variable. + +Example wiring the hook in a Makefile-based build: + +```python +make( + name = "mylib", + lib_source = ":srcs", + out_shared_libs = ["libmylib.so"], + runtime_library_search_directories = "enabled", + # rules_foreign_cc appends the derived rpath flags to these make vars, + # which your Makefile must apply to the shared-library link command. Use a + # dedicated variable here rather than LDFLAGS, which is the default route for + # executable flags. + shared_ldflags_vars = ["SHARED_LDFLAGS"], + targets = ["", "install"], +) +``` + +### Additional search origins + +By default the derived paths assume outputs live at their standard install +locations: `out_binaries` under `out_bin_dir` (defaults to `bin`) and +`out_shared_libs` under `out_lib_dir` (defaults to `lib`). If an output is +loaded from a *non-standard* location within the install tree, declare it so the +right relative paths are derived: + +| Attribute | For | Meaning | +| --- | --- | --- | +| `additional_executable_runtime_library_search_origins` | executables | Extra install-tree-relative directories an executable may be loaded from | +| `additional_dynamic_runtime_library_search_origins` | shared libraries | Extra install-tree-relative directories a shared library may be loaded from | + +Values are relative to the rule's install directory (e.g. `"libexec/bin"`, +`"lib/plugins"`) and should not include the lib name or the rule name. For each +origin, search directories are derived so an object loaded from there can still +find shared libraries from `deps`, `dynamic_deps`, and this rule's own shared +outputs. + +```python +make( + name = "app", + lib_source = ":srcs", + out_binaries = ["app"], + out_shared_libs = ["libfoo.so"], + runtime_library_search_directories = "enabled", + # The binary is also installed/run from libexec/bin, not just bin. + additional_executable_runtime_library_search_origins = ["libexec/bin"], + shared_ldflags_vars = ["SHARED_LDFLAGS"], + targets = ["", "install"], +) +``` + +## Caveats + +- **Resolves inside the Bazel context, not for shipped install trees.** Derived + paths are computed in Bazel's runfiles/execroot coordinate space, and the + entries for `deps` / `dynamic_deps` libraries point at Bazel's layout + (`external//...`, the `_solib_*` symlink farm). They resolve when the + output runs within the Bazel build or its runfiles. If you copy the install + tree somewhere outside Bazel, only a target's `$ORIGIN`-relative references to + its *own* install tree survive — its dependency rpaths will not. + +- **Windows is not supported.** Setting `runtime_library_search_directories = + "enabled"` on a target built with a Windows C++ toolchain is a hard analysis + failure. Leaving it at `"auto"` is safe: global enablement is silently ignored + on Windows. + +- **`ninja` and Boost builds fail when enabled.** These rules do not implement + runtime-search derivation; enabling it (explicitly or via the global setting) + produces an analysis error rather than silently doing nothing. Use + `runtime_library_search_directories = "disabled"` on those targets if you have + enabled the feature globally. + +- **macOS needs `@rpath` install names.** Derived rpaths only take effect if the + installed Mach-O objects reference their dependencies through `@rpath/...` + install names and load commands. This feature passes search directories to + link actions but does **not** rewrite installed dylib IDs or load commands + after the upstream install step. If the upstream build bakes absolute install + names, you must make it emit or preserve `@rpath/...` names (e.g. via the + build's own install-name settings or a post-install fixup). + +- **Shared-lib outputs require the hook attr.** On `make`, `configure_make`, and + `meson`, declaring `out_shared_libs` with the feature enabled but without the + shared-link flag hook (`shared_ldflags_vars` / `shared_ldflags_option`) fails + analysis. Either wire the hook or set + `runtime_library_search_directories = "disabled"`. + +- **Additional origins require qualifying outputs.** Setting an + `additional_*_runtime_library_search_origins` attribute while the feature is + disabled, or with no outputs that need runtime paths (no `out_shared_libs`, + `out_binaries`, `out_data_dirs`, or `out_data_files`), fails analysis — the + attribute would have no effect. diff --git a/examples/integration_tests/runtime_executable/BUILD.bazel b/examples/integration_tests/runtime_executable/BUILD.bazel new file mode 100644 index 000000000..201e11e6a --- /dev/null +++ b/examples/integration_tests/runtime_executable/BUILD.bazel @@ -0,0 +1,27 @@ +load("@rules_foreign_cc//foreign_cc:defs.bzl", "runtime_executable") +load("@rules_shell//shell:sh_test.bzl", "sh_test") + +runtime_executable( + name = "openssl_runtime_executable", + binary = select({ + "@rules_cc//cc/compiler:msvc-cl": "openssl.exe", + "//conditions:default": "openssl", + }), + foreign_cc_target = "@openssl//:openssl", +) + +sh_test( + name = "openssl_help_test", + size = "small", + srcs = ["openssl_help_test.sh"], + args = ["$(rlocationpath :openssl_runtime_executable)"], + data = [ + ":openssl_runtime_executable", + "@bazel_tools//tools/bash/runfiles", + ], +) + +test_suite( + name = "tests", + tests = [":openssl_help_test"], +) diff --git a/examples/integration_tests/runtime_executable/openssl_help_test.sh b/examples/integration_tests/runtime_executable/openssl_help_test.sh new file mode 100755 index 000000000..4166b2ab8 --- /dev/null +++ b/examples/integration_tests/runtime_executable/openssl_help_test.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +set -euo pipefail + +set +u +f=bazel_tools/tools/bash/runfiles/runfiles.bash +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -d ' ' -f 2-)" 2>/dev/null || \ + source "$0.runfiles/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -d ' ' -f 2-)" 2>/dev/null || { + echo >&2 "cannot find $f" + exit 1 + } +set -u + +if [[ "$#" -ne 1 ]]; then + echo "usage: $0 " >&2 + exit 1 +fi + +openssl="$(rlocation "$1")" +if [[ -z "$openssl" || ! -x "$openssl" ]]; then + echo "openssl runtime executable is not executable: $1" >&2 + exit 1 +fi + +exec "$openssl" help diff --git a/examples/integration_tests/runtime_library_search_directories/BUILD.bazel b/examples/integration_tests/runtime_library_search_directories/BUILD.bazel new file mode 100644 index 000000000..80448f283 --- /dev/null +++ b/examples/integration_tests/runtime_library_search_directories/BUILD.bazel @@ -0,0 +1,472 @@ +load("@rules_foreign_cc//foreign_cc:defs.bzl", "cmake", "configure_make", "make", "meson") +load(":runtime_search_tests.bzl", "runtime_search_paths_test", "runtime_search_test") + +UNIX_ONLY = select({ + "@platforms//os:linux": [], + "@platforms//os:macos": [], + "//conditions:default": ["@platforms//:incompatible"], +}) + +LEAF_SHARED_LIB = select({ + "@platforms//os:macos": ["libleaf.dylib"], + "//conditions:default": ["libleaf.so"], +}) + +MIDDLE_SHARED_LIB = select({ + "@platforms//os:macos": ["libmiddle.dylib"], + "//conditions:default": ["libmiddle.so"], +}) + +MAKE_SHARED_LIBRARY_ARGS = select({ + "@platforms//os:macos": [ + "SHARED_LIBRARY_SUFFIX=dylib", + "SHARED_LIBRARY_LINK_FLAG=-dynamiclib", + "LEAF_INSTALL_NAME=-Wl,-install_name,@rpath/libleaf.dylib", + "MIDDLE_INSTALL_NAME=-Wl,-install_name,@rpath/libmiddle.dylib", + ], + "//conditions:default": [ + "SHARED_LIBRARY_SUFFIX=so", + "SHARED_LIBRARY_LINK_FLAG=-shared", + ], +}) + +filegroup( + name = "srcs", + srcs = glob( + ["**"], + exclude = [ + "BUILD.bazel", + "runtime_search_tests.bzl", + "runtime_search_paths_test.sh", + "runtime_search_test.sh", + ], + ), +) + +cmake( + name = "cmake_leaf", + cache_entries = { + "RUNTIME_CHAIN_COMPONENT": "leaf", + "RUNTIME_TEST_FAMILY": "cmake", + }, + lib_source = ":srcs", + out_include_dir = "", + out_shared_libs = LEAF_SHARED_LIB, + runtime_library_search_directories = "enabled", + target_compatible_with = UNIX_ONLY, +) + +cmake( + name = "cmake_middle", + cache_entries = { + "RUNTIME_CHAIN_COMPONENT": "middle", + "RUNTIME_TEST_FAMILY": "cmake", + }, + lib_source = ":srcs", + out_include_dir = "", + out_shared_libs = MIDDLE_SHARED_LIB, + runtime_library_search_directories = "enabled", + target_compatible_with = UNIX_ONLY, + deps = [":cmake_leaf"], +) + +cmake( + name = "cmake_app", + cache_entries = { + "RUNTIME_CHAIN_COMPONENT": "app", + "RUNTIME_TEST_FAMILY": "cmake", + }, + lib_source = ":srcs", + out_binaries = ["runtime_app"], + out_include_dir = "", + runtime_library_search_directories = "enabled", + target_compatible_with = UNIX_ONLY, + deps = [":cmake_middle"], +) + +cmake( + name = "cmake_app_bundle", + cache_entries = { + "RUNTIME_CHAIN_COMPONENT": "bundle", + "RUNTIME_TEST_FAMILY": "cmake_app_bundle", + }, + lib_source = ":srcs", + out_binaries = ["runtime_app"], + out_include_dir = "", + out_shared_libs = LEAF_SHARED_LIB + MIDDLE_SHARED_LIB, + runtime_library_search_directories = "enabled", + target_compatible_with = UNIX_ONLY, +) + +runtime_search_test( + name = "cmake_test", + family = "cmake", + target = ":cmake_app", +) + +runtime_search_test( + name = "cmake_app_bundle_test", + family = "cmake_app_bundle", + target = ":cmake_app_bundle", +) + +runtime_search_paths_test( + name = "cmake_middle_runpath_test", + target = ":cmake_middle", +) + +runtime_search_paths_test( + name = "cmake_app_bundle_runpath_test", + target = ":cmake_app_bundle", +) + +make( + name = "make_leaf", + args = ["RUNTIME_TEST_FAMILY=make"] + MAKE_SHARED_LIBRARY_ARGS, + lib_source = ":srcs", + out_include_dir = "", + out_shared_libs = LEAF_SHARED_LIB, + runtime_library_search_directories = "enabled", + shared_ldflags_vars = ["LDFLAGS"], + target_compatible_with = UNIX_ONLY, + targets = [ + "leaf", + "install-leaf", + ], +) + +make( + name = "make_middle", + args = ["RUNTIME_TEST_FAMILY=make"] + MAKE_SHARED_LIBRARY_ARGS, + lib_source = ":srcs", + out_include_dir = "", + out_shared_libs = MIDDLE_SHARED_LIB, + runtime_library_search_directories = "enabled", + shared_ldflags_vars = ["LDFLAGS"], + target_compatible_with = UNIX_ONLY, + targets = [ + "middle", + "install-middle", + ], + deps = [":make_leaf"], +) + +make( + name = "make_app", + args = ["RUNTIME_TEST_FAMILY=make"] + MAKE_SHARED_LIBRARY_ARGS, + executable_ldflags_vars = ["LDFLAGS"], + lib_source = ":srcs", + out_binaries = ["runtime_app"], + out_include_dir = "", + runtime_library_search_directories = "enabled", + target_compatible_with = UNIX_ONLY, + targets = [ + "app", + "install-app", + ], + deps = [":make_middle"], +) + +make( + name = "make_app_bundle", + args = ["RUNTIME_TEST_FAMILY=make_app_bundle"] + MAKE_SHARED_LIBRARY_ARGS, + executable_ldflags_vars = ["EXECUTABLE_LDFLAGS"], + lib_source = ":srcs", + out_binaries = ["runtime_app"], + out_include_dir = "", + out_shared_libs = LEAF_SHARED_LIB + MIDDLE_SHARED_LIB, + runtime_library_search_directories = "enabled", + shared_ldflags_vars = ["SHARED_LDFLAGS"], + target_compatible_with = UNIX_ONLY, + targets = [ + "app", + "install-leaf", + "install-middle", + "install-app", + ], +) + +runtime_search_test( + name = "make_test", + family = "make", + target = ":make_app", +) + +runtime_search_test( + name = "make_app_bundle_test", + family = "make_app_bundle", + target = ":make_app_bundle", +) + +runtime_search_paths_test( + name = "make_middle_runpath_test", + target = ":make_middle", +) + +runtime_search_paths_test( + name = "make_app_bundle_runpath_test", + target = ":make_app_bundle", +) + +# Exercises additional_executable_runtime_library_search_origins end-to-end. A +# bundle (binary plus its own installed shared libs) so the derived path is the +# stable self lib dir: an executable relocated to libexec/bin must still find the +# bundle's shared libs at /lib. +make( + name = "make_app_additional_origins", + additional_executable_runtime_library_search_origins = ["libexec/bin"], + args = ["RUNTIME_TEST_FAMILY=make_app_bundle"] + MAKE_SHARED_LIBRARY_ARGS, + executable_ldflags_vars = ["EXECUTABLE_LDFLAGS"], + lib_source = ":srcs", + out_binaries = ["runtime_app"], + out_include_dir = "", + out_shared_libs = LEAF_SHARED_LIB + MIDDLE_SHARED_LIB, + runtime_library_search_directories = "enabled", + shared_ldflags_vars = ["SHARED_LDFLAGS"], + target_compatible_with = UNIX_ONLY, + targets = [ + "app", + "install-leaf", + "install-middle", + "install-app", + ], +) + +# Exercises additional_dynamic_runtime_library_search_origins end-to-end, and +# folds in an out_data_dirs output so a declared data dir is also built, installed +# and shown not to perturb the derived wiring. A shared lib loaded from +# lib/plugins must find this rule's own shared outputs at /lib. +make( + name = "make_middle_additional_origins", + additional_dynamic_runtime_library_search_origins = ["lib/plugins"], + args = ["RUNTIME_TEST_FAMILY=make"] + MAKE_SHARED_LIBRARY_ARGS, + lib_source = ":srcs", + out_data_dirs = ["lib/plugins"], + out_include_dir = "", + out_shared_libs = MIDDLE_SHARED_LIB, + runtime_library_search_directories = "enabled", + shared_ldflags_vars = ["LDFLAGS"], + target_compatible_with = UNIX_ONLY, + targets = [ + "middle", + "install-middle", + "install-plugin", + ], + deps = [":make_leaf"], +) + +runtime_search_paths_test( + name = "make_app_additional_origins_test", + # bin -> lib (../lib) is the baseline self path; libexec/bin -> lib + # (../../lib) is derived from the additional executable origin. + expected_additional_binary_runtime_paths = ["../../lib"], + target = ":make_app_additional_origins", +) + +runtime_search_paths_test( + name = "make_middle_additional_origins_test", + # lib/plugins -> lib == .. derived from the additional dynamic origin + # (lib/plugins is one level below the self lib dir). + expected_additional_library_runtime_paths = [".."], + target = ":make_middle_additional_origins", +) + +configure_make( + name = "configure_make_leaf", + args = MAKE_SHARED_LIBRARY_ARGS, + configure_options = ["--family=configure_make"], + configure_prefix = "bash", + lib_source = ":srcs", + out_include_dir = "", + out_shared_libs = LEAF_SHARED_LIB, + runtime_library_search_directories = "enabled", + shared_ldflags_vars = ["LDFLAGS"], + target_compatible_with = UNIX_ONLY, + targets = [ + "leaf", + "install-leaf", + ], +) + +configure_make( + name = "configure_make_middle", + args = MAKE_SHARED_LIBRARY_ARGS, + configure_options = ["--family=configure_make"], + configure_prefix = "bash", + lib_source = ":srcs", + out_include_dir = "", + out_shared_libs = MIDDLE_SHARED_LIB, + runtime_library_search_directories = "enabled", + shared_ldflags_vars = ["LDFLAGS"], + target_compatible_with = UNIX_ONLY, + targets = [ + "middle", + "install-middle", + ], + deps = [":configure_make_leaf"], +) + +configure_make( + name = "configure_make_app", + args = MAKE_SHARED_LIBRARY_ARGS, + configure_options = ["--family=configure_make"], + configure_prefix = "bash", + executable_ldflags_vars = ["LDFLAGS"], + lib_source = ":srcs", + out_binaries = ["runtime_app"], + out_include_dir = "", + runtime_library_search_directories = "enabled", + target_compatible_with = UNIX_ONLY, + targets = [ + "app", + "install-app", + ], + deps = [":configure_make_middle"], +) + +configure_make( + name = "configure_make_app_bundle", + args = MAKE_SHARED_LIBRARY_ARGS, + configure_options = ["--family=configure_make_app_bundle"], + configure_prefix = "bash", + executable_ldflags_vars = ["EXECUTABLE_LDFLAGS"], + lib_source = ":srcs", + out_binaries = ["runtime_app"], + out_include_dir = "", + out_shared_libs = LEAF_SHARED_LIB + MIDDLE_SHARED_LIB, + runtime_library_search_directories = "enabled", + shared_ldflags_vars = ["SHARED_LDFLAGS"], + target_compatible_with = UNIX_ONLY, + targets = [ + "app", + "install-leaf", + "install-middle", + "install-app", + ], +) + +runtime_search_test( + name = "configure_make_test", + family = "configure_make", + target = ":configure_make_app", +) + +runtime_search_test( + name = "configure_make_app_bundle_test", + family = "configure_make_app_bundle", + target = ":configure_make_app_bundle", +) + +runtime_search_paths_test( + name = "configure_make_middle_runpath_test", + target = ":configure_make_middle", +) + +runtime_search_paths_test( + name = "configure_make_app_bundle_runpath_test", + target = ":configure_make_app_bundle", +) + +meson( + name = "meson_leaf", + lib_source = ":srcs", + options = { + "runtime_chain_component": "leaf", + "runtime_test_family": "meson", + }, + out_include_dir = "", + out_shared_libs = LEAF_SHARED_LIB, + runtime_library_search_directories = "enabled", + shared_ldflags_option = "shared_ldflags", + target_compatible_with = UNIX_ONLY, +) + +meson( + name = "meson_middle", + lib_source = ":srcs", + options = { + "runtime_chain_component": "middle", + "runtime_test_family": "meson", + }, + out_include_dir = "", + out_shared_libs = MIDDLE_SHARED_LIB, + runtime_library_search_directories = "enabled", + shared_ldflags_option = "shared_ldflags", + target_compatible_with = UNIX_ONLY, + deps = [":meson_leaf"], +) + +meson( + name = "meson_app", + lib_source = ":srcs", + options = { + "runtime_chain_component": "app", + "runtime_test_family": "meson", + }, + out_binaries = ["runtime_app"], + out_include_dir = "", + runtime_library_search_directories = "enabled", + target_compatible_with = UNIX_ONLY, + deps = [":meson_middle"], +) + +meson( + name = "meson_app_bundle", + lib_source = ":srcs", + options = { + "runtime_chain_component": "bundle", + "runtime_test_family": "meson_app_bundle", + }, + out_binaries = ["runtime_app"], + out_include_dir = "", + out_shared_libs = LEAF_SHARED_LIB + MIDDLE_SHARED_LIB, + runtime_library_search_directories = "enabled", + shared_ldflags_option = "shared_ldflags", + target_compatible_with = UNIX_ONLY, +) + +runtime_search_test( + name = "meson_test", + family = "meson", + target = ":meson_app", +) + +runtime_search_test( + name = "meson_app_bundle_test", + family = "meson_app_bundle", + target = ":meson_app_bundle", +) + +runtime_search_paths_test( + name = "meson_middle_runpath_test", + target = ":meson_middle", +) + +runtime_search_paths_test( + name = "meson_app_bundle_runpath_test", + target = ":meson_app_bundle", +) + +test_suite( + name = "tests", + tests = [ + ":cmake_app_bundle_runpath_test", + ":cmake_app_bundle_test", + ":cmake_middle_runpath_test", + ":cmake_test", + ":configure_make_app_bundle_runpath_test", + ":configure_make_app_bundle_test", + ":configure_make_middle_runpath_test", + ":configure_make_test", + ":make_app_additional_origins_test", + ":make_app_bundle_runpath_test", + ":make_app_bundle_test", + ":make_middle_additional_origins_test", + ":make_middle_runpath_test", + ":make_test", + ":meson_app_bundle_runpath_test", + ":meson_app_bundle_test", + ":meson_middle_runpath_test", + ":meson_test", + ], +) diff --git a/examples/integration_tests/runtime_library_search_directories/CMakeLists.txt b/examples/integration_tests/runtime_library_search_directories/CMakeLists.txt new file mode 100644 index 000000000..828d4b18e --- /dev/null +++ b/examples/integration_tests/runtime_library_search_directories/CMakeLists.txt @@ -0,0 +1,50 @@ +cmake_minimum_required(VERSION 3.15) + +project(runtime_search C) + +set(RUNTIME_CHAIN_COMPONENT "" CACHE STRING "Component to build: leaf, middle, or app") +set(RUNTIME_TEST_FAMILY "cmake" CACHE STRING "Rule family marker for the runtime test") + +if(APPLE) + set(CMAKE_INSTALL_NAME_DIR "@rpath") + set(CMAKE_MACOSX_RPATH ON) +endif() + +function(runtime_chain_compile_definitions target) + target_compile_definitions( + ${target} + PRIVATE + RUNTIME_TEST_FAMILY="${RUNTIME_TEST_FAMILY}" + ) + target_include_directories(${target} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") +endfunction() + +if(RUNTIME_CHAIN_COMPONENT STREQUAL "leaf") + add_library(leaf SHARED src/leaf.c) + runtime_chain_compile_definitions(leaf) + install(TARGETS leaf LIBRARY DESTINATION lib) +elseif(RUNTIME_CHAIN_COMPONENT STREQUAL "middle") + find_library(RUNTIME_CHAIN_LEAF_LIBRARY NAMES leaf REQUIRED) + add_library(middle SHARED src/middle.c) + runtime_chain_compile_definitions(middle) + target_link_libraries(middle PRIVATE "${RUNTIME_CHAIN_LEAF_LIBRARY}") + install(TARGETS middle LIBRARY DESTINATION lib) +elseif(RUNTIME_CHAIN_COMPONENT STREQUAL "app") + find_library(RUNTIME_CHAIN_MIDDLE_LIBRARY NAMES middle REQUIRED) + add_executable(runtime_app src/app.c) + runtime_chain_compile_definitions(runtime_app) + target_link_libraries(runtime_app PRIVATE "${RUNTIME_CHAIN_MIDDLE_LIBRARY}") + install(TARGETS runtime_app RUNTIME DESTINATION bin) +elseif(RUNTIME_CHAIN_COMPONENT STREQUAL "bundle") + add_library(leaf SHARED src/leaf.c) + runtime_chain_compile_definitions(leaf) + add_library(middle SHARED src/middle.c) + runtime_chain_compile_definitions(middle) + target_link_libraries(middle PRIVATE leaf) + add_executable(runtime_app src/app.c) + runtime_chain_compile_definitions(runtime_app) + target_link_libraries(runtime_app PRIVATE middle) + install(TARGETS leaf middle runtime_app LIBRARY DESTINATION lib RUNTIME DESTINATION bin) +else() + message(FATAL_ERROR "RUNTIME_CHAIN_COMPONENT must be one of: leaf, middle, app, bundle") +endif() diff --git a/examples/integration_tests/runtime_library_search_directories/Makefile b/examples/integration_tests/runtime_library_search_directories/Makefile new file mode 100644 index 000000000..080a196b3 --- /dev/null +++ b/examples/integration_tests/runtime_library_search_directories/Makefile @@ -0,0 +1,14 @@ +PREFIX ?= $(INSTALL_PREFIX) +PREFIX ?= install +RUNTIME_TEST_FAMILY ?= make +SRC_DIR ?= src + +.PHONY: all clean test + +all: app + +clean: + +test: all + +include runtime_chain_rules.mk diff --git a/examples/integration_tests/runtime_library_search_directories/Makefile.in b/examples/integration_tests/runtime_library_search_directories/Makefile.in new file mode 100644 index 000000000..eafe5c449 --- /dev/null +++ b/examples/integration_tests/runtime_library_search_directories/Makefile.in @@ -0,0 +1,17 @@ +PREFIX := @prefix@ +RUNTIME_TEST_FAMILY := @family@ +SRC_DIR := @srcdir@/src +CC := @cc@ +CFLAGS := @cflags@ +CPPFLAGS := @cppflags@ +LDFLAGS := @ldflags@ + +.PHONY: all clean test + +all: app + +clean: + +test: all + +include @srcdir@/runtime_chain_rules.mk diff --git a/examples/integration_tests/runtime_library_search_directories/README.md b/examples/integration_tests/runtime_library_search_directories/README.md new file mode 100644 index 000000000..9897fe35e --- /dev/null +++ b/examples/integration_tests/runtime_library_search_directories/README.md @@ -0,0 +1,117 @@ +# Runtime library search directories + +This package tests that foreign_cc can add runtime loader search paths for +libraries produced by other foreign_cc targets and for libraries produced by the +same foreign_cc target. + +The tests are intentionally small. They do not try to cover every possible +native or foreign_cc provider shape; that broader matrix lives in +`examples/integration_tests/transitive_matrix`. This package focuses on whether +the exported `cmake`, `make`, `configure_make`, and `meson` rules pass +the right linker flags to the upstream build system when +`runtime_library_search_directories = "enabled"` is set. + +On Linux, the resulting binary and shared libraries use ELF `RUNPATH` or +`RPATH`. On macOS, they use rpath install names. + +## Runtime chain + +All rule families build the same small C dependency chain: + +```text +runtime_app -> libmiddle -> libleaf +``` + +`libleaf` returns a fixed marker string. `libmiddle` calls `libleaf` and prefixes +the marker with the rule family. `runtime_app` calls `libmiddle`, prints the +result, and fails if the result does not match the expected value. + +For example, the CMake dependency-chain test expects: + +```text +cmake: expected libmiddle loaded through rpath -> expected libleaf loaded through rpath +``` + +The runtime test runner clears `LD_LIBRARY_PATH` and `DYLD_LIBRARY_PATH` before +running the binary. A separate runpath test runner inspects the produced binary +and shared libraries so a fallback runtime path cannot hide a missing expected +entry. + +## Test shapes + +Each supported rule family has two test shapes. + +| Test shape | Example target | What it proves | +| ---------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | +| 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. | +| 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. | + +The same shapes are repeated for: + +| Rule family | Dependency-chain test | App-bundle test | +| ---------------- | ----------------------- | ---------------------------------- | +| `cmake` | `cmake_test` | `cmake_app_bundle_test` | +| `make` | `make_test` | `make_app_bundle_test` | +| `configure_make` | `configure_make_test` | `configure_make_app_bundle_test` | +| `meson` | `meson_test` | `meson_app_bundle_test` | + +## Dependency-chain tests + +The dependency-chain tests build each component as a separate foreign_cc target: + +```text +_leaf -> installs libleaf under lib/ +_middle -> installs libmiddle under lib/, depends on _leaf +_app -> installs runtime_app under bin/, depends on _middle +``` + +The important behavior is that runtime search directories are derived from the +foreign_cc dependency graph: + +- `runtime_app` needs a runtime search path to find `libmiddle`. +- `libmiddle` needs a runtime search path to find `libleaf`. +- The test binary must run without `LD_LIBRARY_PATH` or `DYLD_LIBRARY_PATH`. +- The middle shared library must record self, install-tree-to-solib, and solib + sibling runtime search paths. + +## App-bundle tests + +The app-bundle tests build `runtime_app`, `libmiddle`, and `libleaf` from one +foreign_cc target: + +```text +_app_bundle -> installs runtime_app under bin/ + -> installs libmiddle and libleaf under lib/ +``` + +These tests verify the same-target app bundle behavior: + +- a binary installed under `bin/` can find libraries installed under `lib/` +- a shared library installed under `lib/` can find sibling libraries in `lib/` +- the app and middle shared library record the expected same-install-tree + runtime search paths. + +This is useful for upstream projects that produce an executable and companion +shared libraries in the same install tree, but do not set their own relative +runtime search paths. + +## Fixture files + +| File | Purpose | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | +| `CMakeLists.txt` | Builds the chain for the `cmake` rule. | +| `Makefile` | Builds the chain directly for the `make` rule. | +| `configure` and `Makefile.in` | Provide a minimal configure-style wrapper for the `configure_make` rule. | +| `runtime_chain_rules.mk` | Shared Makefile logic used by `make` and `configure_make`. | +| `src/` | C sources for `runtime_app`, `libmiddle`, and `libleaf`. | +| `runtime_search_paths_test.sh` | Shared shell test runner that checks recorded runtime search paths on produced binaries and shared libraries. | +| `runtime_search_test.sh` | Shared shell test runner that resolves the Bazel runfile binary, clears ambient library search variables, and checks the printed marker. | + +## Run + +Run this package from the examples module: + +```bash +cd examples +bazel test //integration_tests/runtime_library_search_directories:tests --test_output=errors +``` diff --git a/examples/integration_tests/runtime_library_search_directories/configure b/examples/integration_tests/runtime_library_search_directories/configure new file mode 100755 index 000000000..4bf285076 --- /dev/null +++ b/examples/integration_tests/runtime_library_search_directories/configure @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +set -euo pipefail + +prefix="" +family="configure_make" + +for arg in "$@"; do + case "$arg" in + --prefix=*) + prefix="${arg#--prefix=}" + ;; + --family=*) + family="${arg#--family=}" + ;; + esac +done + +if [[ -z "$prefix" ]]; then + echo "missing required --prefix option" >&2 + exit 1 +fi + +srcdir="$(cd "$(dirname "$0")" && pwd)" + +escape_sed_replacement() { + printf '%s' "$1" | sed 's/[&|\\]/\\&/g' +} + +sed \ + -e "s|@prefix@|$prefix|g" \ + -e "s|@family@|$family|g" \ + -e "s|@srcdir@|$srcdir|g" \ + -e "s|@cc@|$(escape_sed_replacement "${CC:-cc}")|g" \ + -e "s|@cflags@|$(escape_sed_replacement "${CFLAGS:-}")|g" \ + -e "s|@cppflags@|$(escape_sed_replacement "${CPPFLAGS:-}")|g" \ + -e "s|@ldflags@|$(escape_sed_replacement "${LDFLAGS:-}")|g" \ + "$srcdir/Makefile.in" > Makefile diff --git a/examples/integration_tests/runtime_library_search_directories/fix_darwin_install_names.sh b/examples/integration_tests/runtime_library_search_directories/fix_darwin_install_names.sh new file mode 100755 index 000000000..2cf47b1ad --- /dev/null +++ b/examples/integration_tests/runtime_library_search_directories/fix_darwin_install_names.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Meson rewrites Darwin install names and load commands during `meson install` +# from @rpath references to full absolute paths under Bazel's sandbox install +# directory. This fixture repairs Meson's installed outputs back to @rpath names +# so the test stays focused on foreign_cc runtime search paths. +[[ "$(uname -s)" == "Darwin" ]] || exit 0 + +libdir="${MESON_INSTALL_DESTDIR_PREFIX}/lib" +bindir="${MESON_INSTALL_DESTDIR_PREFIX}/bin" + +if [[ -f "$libdir/libleaf.dylib" ]]; then + install_name_tool -id @rpath/libleaf.dylib "$libdir/libleaf.dylib" +fi + +if [[ -f "$libdir/libmiddle.dylib" ]]; then + install_name_tool -id @rpath/libmiddle.dylib "$libdir/libmiddle.dylib" + + leaf_ref="$(otool -L "$libdir/libmiddle.dylib" | awk '/libleaf[.]dylib/ { print $1; exit }')" + if [[ -n "$leaf_ref" && "$leaf_ref" != "@rpath/libleaf.dylib" ]]; then + install_name_tool -change "$leaf_ref" @rpath/libleaf.dylib "$libdir/libmiddle.dylib" + fi +fi + +if [[ -f "$bindir/runtime_app" ]]; then + middle_ref="$(otool -L "$bindir/runtime_app" | awk '/libmiddle[.]dylib/ { print $1; exit }')" + if [[ -n "$middle_ref" && "$middle_ref" != "@rpath/libmiddle.dylib" ]]; then + install_name_tool -change "$middle_ref" @rpath/libmiddle.dylib "$bindir/runtime_app" + fi +fi diff --git a/examples/integration_tests/runtime_library_search_directories/meson.build b/examples/integration_tests/runtime_library_search_directories/meson.build new file mode 100644 index 000000000..28790fa85 --- /dev/null +++ b/examples/integration_tests/runtime_library_search_directories/meson.build @@ -0,0 +1,93 @@ +project('runtime_search', 'c') + +runtime_chain_component = get_option('runtime_chain_component') +runtime_test_family = get_option('runtime_test_family') +shared_ldflags = get_option('shared_ldflags') +c = meson.get_compiler('c') + +dep_lib_dirs = run_command( + 'sh', + '-c', + 'if [ -d "$EXT_BUILD_DEPS" ]; then find "$EXT_BUILD_DEPS" -type d -name lib -print | paste -sd: -; fi', + check: true, +).stdout().strip() +dep_lib_dirs = dep_lib_dirs == '' ? [] : dep_lib_dirs.split(':') + +compile_args = [ + '-DRUNTIME_TEST_FAMILY="@0@"'.format(runtime_test_family), +] +include_dirs = include_directories('src') +install_lib_dir = join_paths(get_option('prefix'), 'lib') +install_bin_dir = join_paths(get_option('prefix'), 'bin') + +# Meson rewrites Darwin install names to install paths during install. This +# fixture repairs them so the test remains focused on runtime search paths. +if host_machine.system() == 'darwin' + meson.add_install_script('fix_darwin_install_names.sh') +endif + +if runtime_chain_component == 'leaf' + shared_library( + 'leaf', + 'src/leaf.c', + c_args: compile_args, + include_directories: include_dirs, + install: true, + install_dir: install_lib_dir, + link_args: shared_ldflags, + ) +elif runtime_chain_component == 'middle' + leaf_dep = c.find_library('leaf', dirs: dep_lib_dirs, required: true) + shared_library( + 'middle', + 'src/middle.c', + c_args: compile_args, + dependencies: leaf_dep, + include_directories: include_dirs, + install: true, + install_dir: install_lib_dir, + link_args: shared_ldflags, + ) +elif runtime_chain_component == 'app' + middle_dep = c.find_library('middle', dirs: dep_lib_dirs, required: true) + executable( + 'runtime_app', + 'src/app.c', + c_args: compile_args, + dependencies: middle_dep, + include_directories: include_dirs, + install: true, + install_dir: install_bin_dir, + ) +elif runtime_chain_component == 'bundle' + leaf_lib = shared_library( + 'leaf', + 'src/leaf.c', + c_args: compile_args, + include_directories: include_dirs, + install: true, + install_dir: install_lib_dir, + link_args: shared_ldflags, + ) + middle_lib = shared_library( + 'middle', + 'src/middle.c', + c_args: compile_args, + include_directories: include_dirs, + install: true, + install_dir: install_lib_dir, + link_args: shared_ldflags, + link_with: leaf_lib, + ) + executable( + 'runtime_app', + 'src/app.c', + c_args: compile_args, + include_directories: include_dirs, + install: true, + install_dir: install_bin_dir, + link_with: middle_lib, + ) +else + error('runtime_chain_component must be one of: leaf, middle, app, bundle') +endif diff --git a/examples/integration_tests/runtime_library_search_directories/meson_options.txt b/examples/integration_tests/runtime_library_search_directories/meson_options.txt new file mode 100644 index 000000000..25c36e88d --- /dev/null +++ b/examples/integration_tests/runtime_library_search_directories/meson_options.txt @@ -0,0 +1,3 @@ +option('runtime_chain_component', type: 'string', value: '') +option('runtime_test_family', type: 'string', value: 'meson') +option('shared_ldflags', type: 'array', value: []) diff --git a/examples/integration_tests/runtime_library_search_directories/runtime_chain_rules.mk b/examples/integration_tests/runtime_library_search_directories/runtime_chain_rules.mk new file mode 100644 index 000000000..7e2720d00 --- /dev/null +++ b/examples/integration_tests/runtime_library_search_directories/runtime_chain_rules.mk @@ -0,0 +1,63 @@ +BUILD_DIR ?= build +CC ?= cc +CFLAGS ?= +CPPFLAGS ?= +LDFLAGS ?= +EXECUTABLE_LDFLAGS ?= $(LDFLAGS) +SHARED_LDFLAGS ?= $(LDFLAGS) +SHARED_LIBRARY_SUFFIX ?= so +SHARED_LIBRARY_LINK_FLAG ?= -shared +LEAF_INSTALL_NAME ?= +MIDDLE_INSTALL_NAME ?= + +RUNTIME_CHAIN_CPPFLAGS = $(CPPFLAGS) -I$(SRC_DIR) -DRUNTIME_TEST_FAMILY=\"$(RUNTIME_TEST_FAMILY)\" +LEAF_SHARED_LIBRARY = libleaf.$(SHARED_LIBRARY_SUFFIX) +MIDDLE_SHARED_LIBRARY = libmiddle.$(SHARED_LIBRARY_SUFFIX) +LEAF_SHARED_LIBRARY_PATH = $(BUILD_DIR)/lib/$(LEAF_SHARED_LIBRARY) +MIDDLE_SHARED_LIBRARY_PATH = $(BUILD_DIR)/lib/$(MIDDLE_SHARED_LIBRARY) + +.PHONY: all leaf middle app install-leaf install-middle install-app install-plugin clean test + +all: app + +test: all + +leaf: $(LEAF_SHARED_LIBRARY_PATH) + +middle: leaf $(MIDDLE_SHARED_LIBRARY_PATH) + +app: middle $(BUILD_DIR)/bin/runtime_app + +$(LEAF_SHARED_LIBRARY_PATH): $(SRC_DIR)/leaf.c $(SRC_DIR)/runtime_chain.h + mkdir -p $(BUILD_DIR)/lib + $(CC) $(CFLAGS) $(RUNTIME_CHAIN_CPPFLAGS) -fPIC $(SHARED_LIBRARY_LINK_FLAG) $(SRC_DIR)/leaf.c -o $@ $(LEAF_INSTALL_NAME) $(SHARED_LDFLAGS) + +$(MIDDLE_SHARED_LIBRARY_PATH): $(SRC_DIR)/middle.c $(SRC_DIR)/runtime_chain.h $(LEAF_SHARED_LIBRARY_PATH) + mkdir -p $(BUILD_DIR)/lib + $(CC) $(CFLAGS) $(RUNTIME_CHAIN_CPPFLAGS) -fPIC $(SHARED_LIBRARY_LINK_FLAG) $(SRC_DIR)/middle.c -o $@ $(MIDDLE_INSTALL_NAME) -L$(BUILD_DIR)/lib $(SHARED_LDFLAGS) -lleaf + +$(BUILD_DIR)/bin/runtime_app: $(SRC_DIR)/app.c $(SRC_DIR)/runtime_chain.h $(MIDDLE_SHARED_LIBRARY_PATH) + mkdir -p $(BUILD_DIR)/bin + $(CC) $(CFLAGS) $(RUNTIME_CHAIN_CPPFLAGS) $(SRC_DIR)/app.c -o $@ -L$(BUILD_DIR)/lib $(EXECUTABLE_LDFLAGS) -lmiddle + +install-leaf: leaf + mkdir -p $(PREFIX)/lib + cp -p $(LEAF_SHARED_LIBRARY_PATH) $(PREFIX)/lib/ + +install-middle: middle + mkdir -p $(PREFIX)/lib + cp -p $(MIDDLE_SHARED_LIBRARY_PATH) $(PREFIX)/lib/ + +install-app: app + mkdir -p $(PREFIX)/bin + cp -p $(BUILD_DIR)/bin/runtime_app $(PREFIX)/bin/ + +# Installs a shared library into a data subdirectory of lib so a target can +# declare an out_data_dirs output alongside an additional dynamic origin that +# points into it. +install-plugin: leaf + mkdir -p $(PREFIX)/lib/plugins + cp -p $(LEAF_SHARED_LIBRARY_PATH) $(PREFIX)/lib/plugins/ + +clean: + rm -rf $(BUILD_DIR) diff --git a/examples/integration_tests/runtime_library_search_directories/runtime_search_paths_test.sh b/examples/integration_tests/runtime_library_search_directories/runtime_search_paths_test.sh new file mode 100755 index 000000000..438ac99e3 --- /dev/null +++ b/examples/integration_tests/runtime_library_search_directories/runtime_search_paths_test.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash + +set -euo pipefail + +set +u +f=bazel_tools/tools/bash/runfiles/runfiles.bash +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -d ' ' -f 2-)" 2>/dev/null || \ + source "$0.runfiles/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -d ' ' -f 2-)" 2>/dev/null || { + echo >&2 "cannot find $f" + exit 1 + } +set -u + +runtime_paths() { + case "$OSTYPE" in + darwin*) + otool -l "$1" | + sed -n 's/^[[:space:]]*path \([^ ]*\) (offset [0-9]*)$/\1/p' + ;; + *) + readelf -d "$1" | + sed -n \ + -e 's/.*(RPATH).*Library rpath: \[\(.*\)\]/\1/p' \ + -e 's/.*(RUNPATH).*Library runpath: \[\(.*\)\]/\1/p' | + tr ':' '\n' + ;; + esac +} + +assert_path() { + local file="$1" + local expected="$2" + local actual + + actual="$(runtime_paths "$file")" + if ! grep -Fx "$expected" <<< "$actual" >/dev/null; then + echo >&2 "missing runtime search path" + echo >&2 "file: $file" + echo >&2 "expected: $expected" + echo >&2 "actual:" + printf '%s\n' "$actual" >&2 + exit 1 + fi +} + +assert_path_prefix() { + local file="$1" + local expected="$2" + local actual + local path + + actual="$(runtime_paths "$file")" + while IFS= read -r path; do + if [[ "$path" == "$expected"* ]]; then + return + fi + done <<< "$actual" + + echo >&2 "missing runtime search path prefix" + echo >&2 "file: $file" + echo >&2 "expected prefix: $expected" + echo >&2 "actual:" + printf '%s\n' "$actual" >&2 + exit 1 +} + +case "$OSTYPE" in + darwin*) + loader="@loader_path" + ;; + *) + loader='$ORIGIN' + ;; +esac + +app_runtime_path="$loader/../lib" +middle_runtime_path="$loader/." +solib_path="$loader/../../../../_solib_" +solib_sibling_path="$loader/../_" + +app_files=() +middle_files=() + +for runfile in ${FILES:?FILES must be set}; do + file="$(rlocation "$runfile")" + if [[ -z "$file" || ! -f "$file" ]]; then + continue + fi + + case "$(basename "$runfile")" in + runtime_app) + app_files+=("$file") + ;; + libmiddle.so|libmiddle.dylib) + middle_files+=("$file") + ;; + esac +done + +if [[ "${#app_files[@]}" -eq 0 && "${#middle_files[@]}" -eq 0 ]]; then + echo >&2 "FILES contains no runtime_app or libmiddle output" + exit 1 +fi + +# macOS still ships Bash 3.2, where expanding an empty array under `set -u` +# fails even when the array was initialized. +if [[ "${#app_files[@]}" -gt 0 ]]; then + for file in "${app_files[@]}"; do + assert_path "$file" "$app_runtime_path" + done +fi + +if [[ "${#middle_files[@]}" -gt 0 ]]; then + for file in "${middle_files[@]}"; do + assert_path "$file" "$middle_runtime_path" + if [[ "${#app_files[@]}" -eq 0 ]]; then + assert_path_prefix "$file" "$solib_path" + assert_path_prefix "$file" "$solib_sibling_path" + fi + done +fi + +# Loader-relative paths the target is expected to carry in addition to the +# baseline paths above, e.g. those derived from +# additional_*_runtime_library_search_origins. Asserted per output kind, because +# an executable origin only affects binaries and a dynamic origin only affects +# shared libraries: EXPECTED_ADDITIONAL_BINARY_RUNTIME_PATHS against runtime_app, +# EXPECTED_ADDITIONAL_LIBRARY_RUNTIME_PATHS against libmiddle. Each is checked +# for presence; the binary/library may carry other paths too. +assert_additional_paths() { + local expected_paths="$1" + shift + local rel file + + for rel in $expected_paths; do + for file in "$@"; do + assert_path "$file" "$loader/$rel" + done + done +} + +if [[ -n "${EXPECTED_ADDITIONAL_BINARY_RUNTIME_PATHS:-}" && "${#app_files[@]}" -gt 0 ]]; then + assert_additional_paths "$EXPECTED_ADDITIONAL_BINARY_RUNTIME_PATHS" "${app_files[@]}" +fi + +if [[ -n "${EXPECTED_ADDITIONAL_LIBRARY_RUNTIME_PATHS:-}" && "${#middle_files[@]}" -gt 0 ]]; then + assert_additional_paths "$EXPECTED_ADDITIONAL_LIBRARY_RUNTIME_PATHS" "${middle_files[@]}" +fi diff --git a/examples/integration_tests/runtime_library_search_directories/runtime_search_test.sh b/examples/integration_tests/runtime_library_search_directories/runtime_search_test.sh new file mode 100755 index 000000000..3a6f434f9 --- /dev/null +++ b/examples/integration_tests/runtime_library_search_directories/runtime_search_test.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +set -euo pipefail + +set +u +f=bazel_tools/tools/bash/runfiles/runfiles.bash +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -d ' ' -f 2-)" 2>/dev/null || \ + source "$0.runfiles/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -d ' ' -f 2-)" 2>/dev/null || { + echo >&2 "cannot find $f" + exit 1 + } +set -u + +if [[ "$#" -lt 2 ]]; then + echo "usage: $0 [runfile-path ...]" >&2 + exit 1 +fi + +family="$1" +shift + +binary_runfile_path="" +for arg in "$@"; do + read -r -a candidates <<< "$arg" + for candidate in "${candidates[@]}"; do + case "$candidate" in + */runtime_app) + binary_runfile_path="$candidate" + break 2 + ;; + esac + done +done + +if [[ -z "$binary_runfile_path" ]]; then + echo "runtime test binary runfile path not found in: $*" >&2 + exit 1 +fi + +binary="$(rlocation "$binary_runfile_path")" +if [[ -z "$binary" || ! -x "$binary" ]]; then + echo "runtime test binary is not executable: $binary_runfile_path" >&2 + exit 1 +fi + +stdout_file="$TEST_TMPDIR/runtime-search-stdout.txt" +stderr_file="$TEST_TMPDIR/runtime-search-stderr.txt" + +set +e +env -u LD_LIBRARY_PATH -u DYLD_LIBRARY_PATH "$binary" >"$stdout_file" 2>"$stderr_file" +status="$?" +set -e + +if [[ "$status" -ne 0 ]]; then + echo "runtime test binary failed with exit code $status" >&2 + cat "$stderr_file" >&2 + exit "$status" +fi + +expected="$family: expected libmiddle loaded through rpath -> expected libleaf loaded through rpath" +actual="$(cat "$stdout_file")" + +if [[ "$actual" != "$expected" ]]; then + echo "unexpected runtime marker" >&2 + echo "expected: $expected" >&2 + echo "actual: $actual" >&2 + cat "$stderr_file" >&2 + exit 1 +fi diff --git a/examples/integration_tests/runtime_library_search_directories/runtime_search_tests.bzl b/examples/integration_tests/runtime_library_search_directories/runtime_search_tests.bzl new file mode 100644 index 000000000..2b63937e5 --- /dev/null +++ b/examples/integration_tests/runtime_library_search_directories/runtime_search_tests.bzl @@ -0,0 +1,65 @@ +"""Test macros for the runtime library search directory integration fixture.""" + +load("@rules_shell//shell:sh_test.bzl", "sh_test") + +def runtime_search_test(name, family, target): + """Runs the installed binary with the dynamic loader env cleared. + + Asserts the runtime_app binary built by `target` loads its shared-library + chain purely through baked-in runtime search paths (rpaths), with + LD_LIBRARY_PATH/DYLD_LIBRARY_PATH unset. + + Args: + name: Test target name. + family: Rule-family marker the binary prints (e.g. "cmake", "make"). + target: foreign_cc target producing the runtime_app binary. + """ + sh_test( + name = name, + size = "small", + srcs = ["runtime_search_test.sh"], + args = [ + family, + "$(rlocationpaths %s)" % target, + ], + data = [ + target, + "@bazel_tools//tools/bash/runfiles", + ], + target_compatible_with = select({ + "@platforms//os:linux": [], + "@platforms//os:macos": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + ) + +def runtime_search_paths_test( + name, + target, + expected_additional_binary_runtime_paths = [], + expected_additional_library_runtime_paths = []): + sh_test( + name = name, + size = "small", + srcs = ["runtime_search_paths_test.sh"], + data = [ + target, + "@bazel_tools//tools/bash/runfiles", + ], + env = { + # Loader-relative paths (no $ORIGIN/@loader_path prefix); the test + # script prepends the per-OS loader token and asserts each one is + # present in addition to the baseline paths (membership, not the + # full set). Binary paths are checked against runtime_app, library + # paths against libmiddle, mirroring the executable-vs-dynamic origin + # split. + "EXPECTED_ADDITIONAL_BINARY_RUNTIME_PATHS": " ".join(expected_additional_binary_runtime_paths), + "EXPECTED_ADDITIONAL_LIBRARY_RUNTIME_PATHS": " ".join(expected_additional_library_runtime_paths), + "FILES": "$(rlocationpaths %s)" % target, + }, + target_compatible_with = select({ + "@platforms//os:linux": [], + "@platforms//os:macos": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + ) diff --git a/examples/integration_tests/runtime_library_search_directories/src/app.c b/examples/integration_tests/runtime_library_search_directories/src/app.c new file mode 100644 index 000000000..868408fdf --- /dev/null +++ b/examples/integration_tests/runtime_library_search_directories/src/app.c @@ -0,0 +1,23 @@ +#include +#include + +#include "runtime_chain.h" + +#ifndef RUNTIME_TEST_FAMILY +#define RUNTIME_TEST_FAMILY "unknown" +#endif + +int main(void) { + const char* actual = middle_marker(); + const char* expected = RUNTIME_TEST_FAMILY + ": expected libmiddle loaded through rpath -> expected libleaf loaded " + "through rpath"; + + puts(actual); + if (strcmp(actual, expected) != 0) { + fprintf(stderr, "expected: %s\n", expected); + fprintf(stderr, "actual: %s\n", actual); + return 1; + } + return 0; +} diff --git a/examples/integration_tests/runtime_library_search_directories/src/leaf.c b/examples/integration_tests/runtime_library_search_directories/src/leaf.c new file mode 100644 index 000000000..3a50e26a0 --- /dev/null +++ b/examples/integration_tests/runtime_library_search_directories/src/leaf.c @@ -0,0 +1,5 @@ +#include "runtime_chain.h" + +const char* leaf_marker(void) { + return "expected libleaf loaded through rpath"; +} diff --git a/examples/integration_tests/runtime_library_search_directories/src/middle.c b/examples/integration_tests/runtime_library_search_directories/src/middle.c new file mode 100644 index 000000000..2ced6a308 --- /dev/null +++ b/examples/integration_tests/runtime_library_search_directories/src/middle.c @@ -0,0 +1,16 @@ +#include + +#include "runtime_chain.h" + +#ifndef RUNTIME_TEST_FAMILY +#define RUNTIME_TEST_FAMILY "unknown" +#endif + +const char* middle_marker(void) { + static char marker[256]; + + snprintf(marker, sizeof(marker), + "%s: expected libmiddle loaded through rpath -> %s", + RUNTIME_TEST_FAMILY, leaf_marker()); + return marker; +} diff --git a/examples/integration_tests/runtime_library_search_directories/src/runtime_chain.h b/examples/integration_tests/runtime_library_search_directories/src/runtime_chain.h new file mode 100644 index 000000000..921ab315f --- /dev/null +++ b/examples/integration_tests/runtime_library_search_directories/src/runtime_chain.h @@ -0,0 +1,7 @@ +#ifndef RUNTIME_CHAIN_H_ +#define RUNTIME_CHAIN_H_ + +const char* leaf_marker(void); +const char* middle_marker(void); + +#endif diff --git a/examples/third_party/openssl/BUILD.openssl.bazel b/examples/third_party/openssl/BUILD.openssl.bazel index 3c08cef71..6cee31d35 100644 --- a/examples/third_party/openssl/BUILD.openssl.bazel +++ b/examples/third_party/openssl/BUILD.openssl.bazel @@ -116,10 +116,14 @@ configure_make( "install_name_tool -id @rpath/libcrypto.1.1.dylib $$INSTALLDIR/lib/libcrypto.1.1.dylib", "install_name_tool -change $$BUILD_TMPDIR/openssl/lib/libcrypto.1.1.dylib @rpath/libcrypto.1.1.dylib $$INSTALLDIR/lib/libssl.1.1.dylib", "install_name_tool -change $$INSTALLDIR/lib/libcrypto.1.1.dylib @rpath/libcrypto.1.1.dylib $$INSTALLDIR/lib/libssl.1.1.dylib", + "install_name_tool -change $$BUILD_TMPDIR/openssl/lib/libssl.1.1.dylib @rpath/libssl.1.1.dylib $$INSTALLDIR/bin/openssl", + "install_name_tool -change $$BUILD_TMPDIR/openssl/lib/libcrypto.1.1.dylib @rpath/libcrypto.1.1.dylib $$INSTALLDIR/bin/openssl", ]), "//conditions:default": "", }), resource_size = "enormous", + runtime_library_search_directories = "enabled", + shared_ldflags_vars = ["SHLIB_LDFLAGS"], target_compatible_with = select({ "@platforms//os:windows": ["@platforms//:incompatible"], "//conditions:default": [], diff --git a/foreign_cc/BUILD.bazel b/foreign_cc/BUILD.bazel index 446b65315..6295e4e33 100644 --- a/foreign_cc/BUILD.bazel +++ b/foreign_cc/BUILD.bazel @@ -44,6 +44,7 @@ bzl_library( deps = [ "//foreign_cc/private:detect_root", "//foreign_cc/private:framework", + "//foreign_cc/private:runtime_library_search_directories", ], ) @@ -75,6 +76,7 @@ bzl_library( "//foreign_cc/private:detect_root", "//foreign_cc/private:framework", "//foreign_cc/private:regen_stubs", + "//foreign_cc/private:runtime_library_search_directories", "//foreign_cc/private:transitions", "//toolchains/native_tools:tool_access", "@rules_cc//cc:bzl_srcs", @@ -95,6 +97,7 @@ bzl_library( ":meson", ":msbuild", ":ninja", + ":runtime_executable", ":utils", ], ) @@ -123,6 +126,7 @@ bzl_library( "//foreign_cc/private:detect_root", "//foreign_cc/private:framework", "//foreign_cc/private:make_script", + "//foreign_cc/private:runtime_library_search_directories", "//foreign_cc/private:transitions", "//toolchains/native_tools:tool_access", "@rules_cc//cc:bzl_srcs", @@ -141,6 +145,7 @@ bzl_library( "//foreign_cc/private:detect_root", "//foreign_cc/private:framework", "//foreign_cc/private:make_script", + "//foreign_cc/private:runtime_library_search_directories", "//foreign_cc/private:transitions", "//toolchains/native_tools:native_tools_toolchain", "//toolchains/native_tools:tool_access", @@ -176,6 +181,7 @@ bzl_library( "//foreign_cc/private:detect_root", "//foreign_cc/private:framework", "//foreign_cc/private:ninja_script", + "//foreign_cc/private:runtime_library_search_directories", "//toolchains/native_tools:tool_access", "@rules_cc//cc:bzl_srcs", "@rules_cc//cc/common", @@ -207,3 +213,14 @@ bzl_library( srcs = ["providers.bzl"], visibility = ["//visibility:public"], ) + +bzl_library( + name = "runtime_executable", + srcs = ["runtime_executable.bzl"], + visibility = ["//visibility:public"], + deps = [ + ":providers", + "@bazel_skylib//lib:shell", + "@rules_shell//shell:rules_bzl", + ], +) diff --git a/foreign_cc/boost_build.bzl b/foreign_cc/boost_build.bzl index 8749df4ab..784d76d21 100644 --- a/foreign_cc/boost_build.bzl +++ b/foreign_cc/boost_build.bzl @@ -2,7 +2,7 @@ load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain") load("@rules_cc//cc:defs.bzl", "CcInfo") -load("//foreign_cc/private:cc_toolchain_util.bzl", "absolutize_path_in_str", "get_flags_info", "get_tools_info") +load("//foreign_cc/private:cc_toolchain_util.bzl", "absolutize_path_in_str", "get_flags_info", "get_tools_info", "targets_windows") load("//foreign_cc/private:detect_root.bzl", "detect_root") load( "//foreign_cc/private:framework.bzl", @@ -12,6 +12,7 @@ load( "create_attrs", "expand_locations_and_make_variables", ) +load("//foreign_cc/private:runtime_library_search_directories.bzl", "runtime_library_search_directories_enabled") load("//foreign_cc/private/framework:helpers.bzl", "escape_dquote_bash") def _boost_build_impl(ctx): @@ -69,8 +70,19 @@ def _create_configure_script(configureParameters): data = ctx.attr.data + ctx.attr.build_data user_options = expand_locations_and_make_variables(ctx, ctx.attr.user_options, "user_options", data) - flags = get_flags_info(ctx) cc_toolchain = find_cpp_toolchain(ctx) + runtime_search_enabled = runtime_library_search_directories_enabled( + ctx, + is_windows = targets_windows(ctx, cc_toolchain), + ) + if runtime_search_enabled: + fail(( + "ERROR: {} enables runtime_library_search_directories, but " + + "runtime_library_search_directories is not supported by the " + + "boost_build rule." + ).format(ctx.label)) + + flags = get_flags_info(ctx) tools = get_tools_info(ctx) toolset = _b2_toolset(cc_toolchain.compiler) diff --git a/foreign_cc/built_tools/private/built_tools_framework.bzl b/foreign_cc/built_tools/private/built_tools_framework.bzl index ea958adc1..e13c657ec 100644 --- a/foreign_cc/built_tools/private/built_tools_framework.bzl +++ b/foreign_cc/built_tools/private/built_tools_framework.bzl @@ -5,8 +5,24 @@ load("//foreign_cc/private:cc_toolchain_util.bzl", "absolutize_path_in_str") load("//foreign_cc/private:detect_root.bzl", "detect_root") load("//foreign_cc/private:framework.bzl", "FOREIGN_CC_FRAMEWORK_COMMON_ATTRS", "get_env_prelude", "wrap_outputs") load("//foreign_cc/private:resource_sets.bzl", "get_resource_env_vars") +load("//foreign_cc/private:runtime_library_search_directories.bzl", "RUNTIME_LIBRARY_SEARCH_DIRECTORY_ATTRIBUTES") load("//foreign_cc/private/framework:helpers.bzl", "convert_shell_script", "shebang") +def _without_attrs(attrs, excluded_attrs): + return { + name: value + for name, value in attrs.items() + if name not in excluded_attrs + } + +# Built-tools do not have the regular foreign_cc outputs model needed for +# runtime library search directory derivation. They also don't produce +# shared libs that needs rpath treatment. +_FOREIGN_CC_BUILT_TOOLS_COMMON_ATTRS = _without_attrs( + FOREIGN_CC_FRAMEWORK_COMMON_ATTRS, + RUNTIME_LIBRARY_SEARCH_DIRECTORY_ATTRIBUTES, +) + # Common attributes for all built_tool rules FOREIGN_CC_BUILT_TOOLS_ATTRS = { "configure_xcompile": attr.bool( @@ -19,7 +35,7 @@ FOREIGN_CC_BUILT_TOOLS_ATTRS = { doc = "The target containing the build tool's sources", mandatory = True, ), -} | FOREIGN_CC_FRAMEWORK_COMMON_ATTRS +} | _FOREIGN_CC_BUILT_TOOLS_COMMON_ATTRS # Common fragments for all built_tool rules FOREIGN_CC_BUILT_TOOLS_FRAGMENTS = [ diff --git a/foreign_cc/cmake.bzl b/foreign_cc/cmake.bzl index 35a663022..633949fbe 100644 --- a/foreign_cc/cmake.bzl +++ b/foreign_cc/cmake.bzl @@ -206,7 +206,11 @@ def _create_configure_script(configureParameters): tools = get_tools_info(ctx) # CMake will replace with the actual output file - flags = get_flags_info(ctx, "") + flags = get_flags_info( + ctx, + "", + outputs = configureParameters.outputs, + ) no_toolchain_file = ctx.attr.cache_entries.get("CMAKE_TOOLCHAIN_FILE") or not ctx.attr.generate_crosstool_file cmake_commands = [] diff --git a/foreign_cc/configure.bzl b/foreign_cc/configure.bzl index 379cfedc4..5b7d1d14d 100644 --- a/foreign_cc/configure.bzl +++ b/foreign_cc/configure.bzl @@ -8,6 +8,7 @@ load( "//foreign_cc/private:cc_toolchain_util.bzl", "get_flags_info", "get_tools_info", + "targets_windows", ) load("//foreign_cc/private:configure_script.bzl", "create_configure_script") load("//foreign_cc/private:detect_root.bzl", "detect_root") @@ -21,6 +22,11 @@ load( "expand_locations_and_make_variables", ) load("//foreign_cc/private:regen_stubs.bzl", "REGEN_STUBS") +load( + "//foreign_cc/private:runtime_library_search_directories.bzl", + "enforce_runtime_search_shared_ldflags_attr", + "runtime_library_search_directories_enabled", +) load("//foreign_cc/private:transitions.bzl", "foreign_cc_rule_variant") load( "//toolchains/native_tools:tool_access.bzl", @@ -108,7 +114,22 @@ def _create_configure_script(configureParameters): inputs = configureParameters.inputs tools = get_tools_info(ctx) - flags = get_flags_info(ctx) + cc_toolchain = find_cpp_toolchain(ctx) + runtime_search_enabled = runtime_library_search_directories_enabled( + ctx, + is_windows = targets_windows(ctx, cc_toolchain), + ) + flags = get_flags_info( + ctx, + outputs = configureParameters.outputs, + ) + + enforce_runtime_search_shared_ldflags_attr( + ctx, + runtime_search_enabled, + flags.cxx_linker_shared, + "shared_ldflags_vars", + ) define_install_prefix = ["export INSTALL_PREFIX=\"" + _get_install_prefix(ctx) + "\""] @@ -135,7 +156,6 @@ def _create_configure_script(configureParameters): if xcompile_options: configure_options.extend(xcompile_options) - cc_toolchain = find_cpp_toolchain(ctx) is_msvc = cc_toolchain.compiler == "msvc-cl" configure = create_configure_script( @@ -285,9 +305,10 @@ def _attrs(): ), "shared_ldflags_vars": attr.string_list( doc = ( - "A list of variable names use as LDFLAGS for shared libraries. These variables " + + "A list of variable names used as LDFLAGS for shared libraries. These variables " + "will be passed to the make command as make vars and overwrite what is defined in " + - "the Makefile." + "the Makefile. Required when runtime_library_search_directories is enabled and " + + "out_shared_libs declares shared-library outputs." ), mandatory = False, default = [], diff --git a/foreign_cc/defs.bzl b/foreign_cc/defs.bzl index e7ba6b078..e94360d5a 100644 --- a/foreign_cc/defs.bzl +++ b/foreign_cc/defs.bzl @@ -11,6 +11,7 @@ load(":make.bzl", _make = "make", _make_variant = "make_variant") load(":meson.bzl", _meson = "meson", _meson_with_requirements = "meson_with_requirements") load(":msbuild.bzl", _msbuild = "msbuild") load(":ninja.bzl", _ninja = "ninja") +load(":runtime_executable.bzl", _runtime_executable = "runtime_executable") load(":utils.bzl", _runnable_binary = "runnable_binary") boost_build = _boost_build @@ -25,3 +26,4 @@ meson_with_requirements = _meson_with_requirements msbuild = _msbuild ninja = _ninja runnable_binary = _runnable_binary +runtime_executable = _runtime_executable diff --git a/foreign_cc/make.bzl b/foreign_cc/make.bzl index bcbfc6a41..fc785d5f2 100644 --- a/foreign_cc/make.bzl +++ b/foreign_cc/make.bzl @@ -6,6 +6,7 @@ load( "//foreign_cc/private:cc_toolchain_util.bzl", "get_flags_info", "get_tools_info", + "targets_windows", ) load( "//foreign_cc/private:detect_root.bzl", @@ -20,6 +21,11 @@ load( "expand_locations_and_make_variables", ) load("//foreign_cc/private:make_script.bzl", "create_make_script") +load( + "//foreign_cc/private:runtime_library_search_directories.bzl", + "enforce_runtime_search_shared_ldflags_attr", + "runtime_library_search_directories_enabled", +) load("//foreign_cc/private:transitions.bzl", "foreign_cc_rule_variant") load("//toolchains/native_tools:tool_access.bzl", "get_make_data") @@ -45,7 +51,22 @@ def _create_make_script(configureParameters): root = detect_root(ctx.attr.lib_source) tools = get_tools_info(ctx) - flags = get_flags_info(ctx) + cc_toolchain = find_cpp_toolchain(ctx) + runtime_search_enabled = runtime_library_search_directories_enabled( + ctx, + is_windows = targets_windows(ctx, cc_toolchain), + ) + flags = get_flags_info( + ctx, + outputs = configureParameters.outputs, + ) + + enforce_runtime_search_shared_ldflags_attr( + ctx, + runtime_search_enabled, + flags.cxx_linker_shared, + "shared_ldflags_vars", + ) data = ctx.attr.data + ctx.attr.build_data @@ -68,7 +89,6 @@ def _create_make_script(configureParameters): install_prefix = ctx.attr.install_prefix, )) - cc_toolchain = find_cpp_toolchain(ctx) is_msvc = cc_toolchain.compiler == "msvc-cl" return create_make_script( @@ -124,9 +144,10 @@ def _attrs(): ), "shared_ldflags_vars": attr.string_list( doc = ( - "A string list of variable names use as LDFLAGS for shared libraries. These variables " + + "A string list of variable names used as LDFLAGS for shared libraries. These variables " + "will be passed to the make command as make vars and overwrite what is defined in " + - "the Makefile." + "the Makefile. Required when runtime_library_search_directories is enabled and " + + "out_shared_libs declares shared-library outputs." ), mandatory = False, default = [], diff --git a/foreign_cc/meson.bzl b/foreign_cc/meson.bzl index 277de59bf..ecd9e38f2 100644 --- a/foreign_cc/meson.bzl +++ b/foreign_cc/meson.bzl @@ -1,13 +1,16 @@ """A rule for building projects using the [Meson](https://mesonbuild.com/) build system""" +load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain") load("@rules_cc//cc:defs.bzl", "CcInfo") load("//foreign_cc:utils.bzl", "full_label") load("//foreign_cc/built_tools:meson_build.bzl", "meson_tool") load( "//foreign_cc/private:cc_toolchain_util.bzl", "absolutize_path_in_str", + "escape_loader_tokens_for_shell", "get_flags_info", "get_tools_info", + "targets_windows", ) load( "//foreign_cc/private:detect_root.bzl", @@ -22,6 +25,11 @@ load( "expand_locations_and_make_variables", ) load("//foreign_cc/private:make_script.bzl", "pkgconfig_script") +load( + "//foreign_cc/private:runtime_library_search_directories.bzl", + "enforce_runtime_search_shared_ldflags_attr", + "runtime_library_search_directories_enabled", +) load("//foreign_cc/private:transitions.bzl", "foreign_cc_rule_variant") load("//toolchains/native_tools:native_tools_toolchain.bzl", "native_tool_toolchain") load("//toolchains/native_tools:tool_access.bzl", "get_cmake_data", "get_make_data", "get_meson_data", "get_ninja_data", "get_pkgconfig_data") @@ -71,7 +79,23 @@ def _create_meson_script(configureParameters): inputs = configureParameters.inputs tools = get_tools_info(ctx) - flags = get_flags_info(ctx) + cc_toolchain = find_cpp_toolchain(ctx) + runtime_search_enabled = runtime_library_search_directories_enabled( + ctx, + is_windows = targets_windows(ctx, cc_toolchain), + ) + flags = get_flags_info( + ctx, + outputs = configureParameters.outputs, + ) + + enforce_runtime_search_shared_ldflags_attr( + ctx, + runtime_search_enabled, + flags.cxx_linker_shared, + "shared_ldflags_option", + ) + script = pkgconfig_script(inputs.ext_build_dirs) build_options = ctx.attr.options @@ -155,17 +179,20 @@ def _create_meson_script(configureParameters): target_args[target_name] = args + # Expand options + build_options = expand_locations_and_make_variables(ctx, build_options, "options", data) + # Append shared flags to build options if flags.cxx_linker_shared and ctx.attr.shared_ldflags_option: if ctx.attr.shared_ldflags_option in build_options: fail("cannot override existing build option: {}".format(ctx.attr.shared_ldflags_option)) - absolutized = [_absolutize(ctx.workspace_name, f).replace("$EXT_BUILD_ROOT/", "$$EXT_BUILD_ROOT$$/") for f in flags.cxx_linker_shared] + absolutized = [ + escape_loader_tokens_for_shell(_absolutize(ctx.workspace_name, f).replace("$EXT_BUILD_ROOT/", "$$EXT_BUILD_ROOT$$/")) + for f in flags.cxx_linker_shared + ] build_options = build_options | {ctx.attr.shared_ldflags_option: _list_to_str_repr(absolutized)} - # Expand options - build_options = expand_locations_and_make_variables(ctx, build_options, "options", data) - script.append("{meson} setup --prefix={install_dir} {setup_args} {options} {source_dir}".format( meson = meson_path, install_dir = "$$INSTALLDIR$$", @@ -251,7 +278,11 @@ def _attrs(): mandatory = False, ), "shared_ldflags_option": attr.string( - doc = "Name of additional setup option that will contain shared ldflags.", + doc = ( + "Name of additional setup option that will contain shared ldflags. " + + "Required when runtime_library_search_directories is enabled and " + + "out_shared_libs declares shared-library outputs." + ), mandatory = False, ), "target_args": attr.string_list_dict( @@ -333,7 +364,7 @@ def _absolutize(workspace_name, text, force = False): return absolutize_path_in_str(workspace_name, "$EXT_BUILD_ROOT/", text, force) def _join_flags_list(workspace_name, flags): - return " ".join([_absolutize(workspace_name, flag) for flag in flags]) + return escape_loader_tokens_for_shell(" ".join([_absolutize(workspace_name, flag) for flag in flags])) def _list_to_str_repr(lst): # see https://mesonbuild.com/Build-options.html#using-build-options @@ -346,4 +377,5 @@ def _list_to_str_repr(lst): export_for_test = struct( list_to_str_repr = _list_to_str_repr, + join_flags_list = _join_flags_list, ) diff --git a/foreign_cc/ninja.bzl b/foreign_cc/ninja.bzl index 80becb8cb..151e7da93 100644 --- a/foreign_cc/ninja.bzl +++ b/foreign_cc/ninja.bzl @@ -6,6 +6,7 @@ load( "//foreign_cc/private:cc_toolchain_util.bzl", "get_flags_info", "get_tools_info", + "targets_windows", ) load( "//foreign_cc/private:detect_root.bzl", @@ -20,6 +21,7 @@ load( "expand_locations_and_make_variables", ) load("//foreign_cc/private:ninja_script.bzl", "create_ninja_script") +load("//foreign_cc/private:runtime_library_search_directories.bzl", "runtime_library_search_directories_enabled") load("//toolchains/native_tools:tool_access.bzl", "get_ninja_data") def _ninja_impl(ctx): @@ -60,6 +62,19 @@ def _create_ninja_script(configureParameters): root = detect_root(ctx.attr.lib_source) tools = get_tools_info(ctx) + cc_toolchain = find_cpp_toolchain(ctx) + runtime_search_enabled = runtime_library_search_directories_enabled( + ctx, + is_windows = targets_windows(ctx, cc_toolchain), + ) + + if runtime_search_enabled: + fail(( + "ERROR: {} enables runtime_library_search_directories, but " + + "runtime_library_search_directories is not supported by the ninja " + + "at this time." + ).format(ctx.label)) + flags = get_flags_info(ctx) data = ctx.attr.data + ctx.attr.build_data @@ -81,7 +96,6 @@ def _create_ninja_script(configureParameters): prefix = "{} ".format(expand_locations_and_make_variables(ctx, attrs.tool_prefix, "tool_prefix", data)) if attrs.tool_prefix else "" - cc_toolchain = find_cpp_toolchain(ctx) is_msvc = cc_toolchain.compiler == "msvc-cl" return create_ninja_script( diff --git a/foreign_cc/private/BUILD.bazel b/foreign_cc/private/BUILD.bazel index 5bf850b8a..bf42acd2b 100644 --- a/foreign_cc/private/BUILD.bazel +++ b/foreign_cc/private/BUILD.bazel @@ -2,6 +2,7 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") exports_files([ "runnable_binary_wrapper.sh", + "runtime_executable_wrapper.sh.tpl", "settings.sh.in", ]) @@ -18,12 +19,34 @@ bzl_library( srcs = ["cc_toolchain_util.bzl"], visibility = ["//foreign_cc:__subpackages__"], deps = [ + ":runtime_library_search_directories", "@bazel_skylib//lib:collections", "@bazel_tools//tools/build_defs/cc:action_names.bzl", "@bazel_tools//tools/cpp:toolchain_utils.bzl", ], ) +bzl_library( + name = "runtime_library_search_directories", + srcs = ["runtime_library_search_directories.bzl"], + visibility = ["//foreign_cc:__subpackages__"], + deps = [ + ":runtime_search_paths", + "@bazel_skylib//rules:common_settings", + ], +) + +bzl_library( + name = "runtime_search_paths", + srcs = ["runtime_search_paths.bzl"], + visibility = ["//foreign_cc:__subpackages__"], + deps = [ + "@bazel_skylib//lib:paths", + "@rules_cc//cc:bzl_srcs", + "@rules_cc//cc/common:cc_shared_library_info_bzl", + ], +) + bzl_library( name = "cmake_script", srcs = ["cmake_script.bzl"], @@ -72,6 +95,7 @@ bzl_library( ":detect_xcompile.bzl", ":resource_sets", ":run_shell_file_utils", + ":runtime_library_search_directories", "//foreign_cc:providers", "//foreign_cc/private/framework:helpers", "//foreign_cc/private/framework:platform", @@ -127,7 +151,9 @@ bzl_library( name = "transitions", srcs = ["transitions.bzl"], visibility = ["//foreign_cc:__subpackages__"], - deps = ["//foreign_cc:providers"], + deps = [ + "//foreign_cc:providers", + ], ) bzl_library( diff --git a/foreign_cc/private/cc_toolchain_util.bzl b/foreign_cc/private/cc_toolchain_util.bzl index 7e4e76e99..71b4856f7 100644 --- a/foreign_cc/private/cc_toolchain_util.bzl +++ b/foreign_cc/private/cc_toolchain_util.bzl @@ -7,6 +7,11 @@ load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES") load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain") load("@rules_cc//cc:defs.bzl", "CcInfo", "cc_common") load("//foreign_cc/private/framework:platform.bzl", "target_os_name") +load( + ":runtime_library_search_directories.bzl", + "runtime_library_search_directories", + "runtime_library_search_directories_enabled", +) LibrariesToLinkInfo = provider( doc = "Libraries to be wrapped into CcLinkingInfo", @@ -42,6 +47,32 @@ CxxFlagsInfo = provider( ), ) +_LOADER_TOKEN_ESCAPES_FOR_SHELL = [ + ("$EXEC_ORIGIN", "\\$EXEC_ORIGIN"), + ("$ORIGIN", "\\$ORIGIN"), +] + +_LOADER_TOKEN_ESCAPES_FOR_MAKE = [ + ("$EXEC_ORIGIN", "\\\\$\\$EXEC_ORIGIN"), + ("$ORIGIN", "\\\\$\\$ORIGIN"), +] + +def escape_loader_tokens_for_shell(text): + return _escape_loader_tokens(_normalize_loader_tokens(text), _LOADER_TOKEN_ESCAPES_FOR_SHELL) + +def escape_loader_tokens_for_make(text): + return _escape_loader_tokens(_normalize_loader_tokens(text), _LOADER_TOKEN_ESCAPES_FOR_MAKE) + +def _normalize_loader_tokens(text): + for token, _ in _LOADER_TOKEN_ESCAPES_FOR_SHELL: + text = text.replace("\\" + token, token) + return text + +def _escape_loader_tokens(text, token_escapes): + for token, escaped_token in token_escapes: + text = text.replace(token, escaped_token) + return text + # Since we're calling an external build system we can't support some # features that may be enabled on the toolchain - so we disable # them here when configuring the toolchain flags to pass to the external @@ -128,6 +159,28 @@ def _dynamic_module_link_flags(shared_flags, is_darwin): return [flag for flag in shared_flags if flag not in ["-shared", "-dynamiclib"]] return shared_flags +def _create_link_variables( + *, + cc_toolchain, + feature_configuration, + is_using_linker, + is_linking_dynamic_library, + must_keep_debug, + output_file = None, + runtime_library_search_directories = None): + kwargs = { + "cc_toolchain": cc_toolchain, + "feature_configuration": feature_configuration, + "is_linking_dynamic_library": is_linking_dynamic_library, + "is_using_linker": is_using_linker, + "must_keep_debug": must_keep_debug, + } + if output_file != None: + kwargs["output_file"] = output_file + if runtime_library_search_directories != None: + kwargs["runtime_library_search_directories"] = runtime_library_search_directories + return cc_common.create_link_variables(**kwargs) + def targets_windows(ctx, cc_toolchain): """Returns true if build is targeting Windows @@ -248,13 +301,15 @@ def _set_file_prefix_map_enabled(ctx, cc_toolchain): return True return False -def get_flags_info(ctx, link_output_file = None): +def get_flags_info(ctx, link_output_file = None, outputs = None): """Takes information about flags from cc_toolchain, returns CxxFlagsInfo Args: ctx: rule context link_output_file: output file to be specified in the link command line flags + outputs: framework-declared outputs used to derive runtime library + search directories Returns: CxxFlagsInfo: A provider containing Cxx flags @@ -271,6 +326,15 @@ def get_flags_info(ctx, link_output_file = None): defines = _defines_from_deps(ctx) use_pic = cc_toolchain_.needs_pic_for_dynamic_libraries(feature_configuration = feature_configuration) + runtime_search_enabled = runtime_library_search_directories_enabled( + ctx, + is_windows = targets_windows(ctx, cc_toolchain_), + ) + if not runtime_search_enabled: + runtime_search_directories = struct(shared_dirs = None, executable_dirs = None) + else: + runtime_search_directories = runtime_library_search_directories(ctx, outputs) + flags = CxxFlagsInfo( cc = cc_common.get_memory_inefficient_command_line( feature_configuration = feature_configuration, @@ -296,19 +360,20 @@ def get_flags_info(ctx, link_output_file = None): cxx_linker_shared = cc_common.get_memory_inefficient_command_line( feature_configuration = feature_configuration, action_name = ACTION_NAMES.cpp_link_dynamic_library, - variables = cc_common.create_link_variables( + variables = _create_link_variables( cc_toolchain = cc_toolchain_, feature_configuration = feature_configuration, is_using_linker = True, is_linking_dynamic_library = True, must_keep_debug = False, + runtime_library_search_directories = runtime_search_directories.shared_dirs, ), ), cxx_linker_dynamic_module = [], cxx_linker_static = cc_common.get_memory_inefficient_command_line( feature_configuration = feature_configuration, action_name = ACTION_NAMES.cpp_link_static_library, - variables = cc_common.create_link_variables( + variables = _create_link_variables( cc_toolchain = cc_toolchain_, feature_configuration = feature_configuration, is_using_linker = False, @@ -320,12 +385,13 @@ def get_flags_info(ctx, link_output_file = None): cxx_linker_executable = cc_common.get_memory_inefficient_command_line( feature_configuration = feature_configuration, action_name = ACTION_NAMES.cpp_link_executable, - variables = cc_common.create_link_variables( + variables = _create_link_variables( cc_toolchain = cc_toolchain_, feature_configuration = feature_configuration, is_using_linker = True, is_linking_dynamic_library = False, must_keep_debug = False, + runtime_library_search_directories = runtime_search_directories.executable_dirs, ), ), assemble = cc_common.get_memory_inefficient_command_line( diff --git a/foreign_cc/private/cmake_script.bzl b/foreign_cc/private/cmake_script.bzl index ffeb6993d..4420bc908 100644 --- a/foreign_cc/private/cmake_script.bzl +++ b/foreign_cc/private/cmake_script.bzl @@ -1,7 +1,7 @@ """ Contains all logic for calling CMake for building external libraries/binaries """ load("//foreign_cc/private:make_script.bzl", "pkgconfig_script") -load(":cc_toolchain_util.bzl", "absolutize_path_in_str") +load(":cc_toolchain_util.bzl", "absolutize_path_in_str", "escape_loader_tokens_for_shell") def _escape_dquote_bash(text): """ Escape double quotes in flag lists for use in bash strings that set environment variables """ @@ -537,12 +537,13 @@ def _absolutize(workspace_name, text, force = False): return absolutize_path_in_str(workspace_name, "$${EXT_BUILD_ROOT//\\\\//}$$/", text, force) def _join_flags_list(workspace_name, flags): - return " ".join([_absolutize(workspace_name, flag) for flag in flags]) + return escape_loader_tokens_for_shell(" ".join([_absolutize(workspace_name, flag) for flag in flags])) export_for_test = struct( absolutize = _absolutize, tail_if_starts_with = _tail_if_starts_with, find_flag_value = _find_flag_value, + join_flags_list = _join_flags_list, fill_crossfile_from_toolchain = _fill_crossfile_from_toolchain, move_dict_values = _move_dict_values, reverse_descriptor_dict = _reverse_descriptor_dict, diff --git a/foreign_cc/private/configure_script.bzl b/foreign_cc/private/configure_script.bzl index 693a75c81..8ed7e00e4 100644 --- a/foreign_cc/private/configure_script.bzl +++ b/foreign_cc/private/configure_script.bzl @@ -93,10 +93,20 @@ def create_configure_script( if regen_stub_prefix: regen_stub_prefix += " " - make_commands = [] + # The "make" expansion context here is to allow runtime search path with $ORIGIN to be properly escaped + # for Makefiles. The actual nuance is a bit subtle. + # If we pass "\$ORIGIN" in LDFLAGS to configure, it will see "$ORIGIN" and this will end up as "RIGIN" + # in the generated Makefile. + # If we pass "\\$\$ORIGIN" in LDFLAGS to configure, it will see "\$$ORIGIN" and this will end up as + # "$$ORIGIN" in the generated Makefile. This is what we want. + # However, that latter option does mean configure probes will see "\$$ORIGIN" in RUNPATH of a built + # binary. This is ok since these RUNPATH are never intended for configure probes. Also an incorrect + # RUNPATH does not fail execution and will move on to the next defined RUNPATH. + configure_env_vars = get_make_env_vars(workspace_name, tools, flags, env_vars, deps, inputs, is_msvc, [], expansion_context = "make") + script.append("{regen_stubs}{env_vars} {prefix}\"{configure}\" {prefix_flag}$$BUILD_TMPDIR$$/$$INSTALL_PREFIX$$ {user_options}".format( regen_stubs = regen_stub_prefix, - env_vars = get_make_env_vars(workspace_name, tools, flags, env_vars, deps, inputs, is_msvc, make_commands), + env_vars = configure_env_vars, prefix = configure_prefix, configure = configure_path, prefix_flag = prefix_flag, diff --git a/foreign_cc/private/framework.bzl b/foreign_cc/private/framework.bzl index b822e2b27..1ab37cd8d 100644 --- a/foreign_cc/private/framework.bzl +++ b/foreign_cc/private/framework.bzl @@ -8,7 +8,7 @@ load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain") load("@rules_cc//cc:defs.bzl", "CcInfo", "cc_common") load("@rules_cc//cc/common:cc_shared_library_info.bzl", "CcSharedLibraryInfo") -load("//foreign_cc:providers.bzl", "ForeignCcArtifactInfo", "ForeignCcDepsInfo") +load("//foreign_cc:providers.bzl", "ForeignCcArtifactInfo", "ForeignCcDepsInfo", "ForeignCcRuntimeExecutableInfo") load("//foreign_cc/private:detect_root.bzl", "filter_containing_dirs_from_inputs") load("//foreign_cc/private:resource_sets.bzl", "SIZE_ATTRIBUTES", "get_resource_env_vars") load( @@ -31,6 +31,7 @@ load( ":run_shell_file_utils.bzl", "copy_directory", ) +load(":runtime_library_search_directories.bzl", "RUNTIME_LIBRARY_SEARCH_DIRECTORY_ATTRIBUTES") # Attributes shared by every framework rule (regular `cc_external_rule_impl`-based # rules and `built_tools` rules alike). @@ -82,7 +83,7 @@ FOREIGN_CC_FRAMEWORK_COMMON_ATTRS = { default = Label("@rules_foreign_cc//foreign_cc/settings:set_file_prefix_map_default"), providers = [BuildSettingInfo], ), -} | PLATFORM_CONSTRAINTS_RULE_ATTRIBUTES | SIZE_ATTRIBUTES +} | PLATFORM_CONSTRAINTS_RULE_ATTRIBUTES | SIZE_ATTRIBUTES | RUNTIME_LIBRARY_SEARCH_DIRECTORY_ATTRIBUTES # Dict with definitions of the context attributes, that customize cc_external_rule_impl function. # Many of the attributes have default values. @@ -322,6 +323,7 @@ of the script, and allows to reuse the inputs structure, created by the framewor inputs = """InputFiles provider: summarized information on rule inputs, created by framework function, to be reused in script creator. Contains in particular merged compilation and linking dependencies.""", + outputs = "Declared outputs created by the framework.", ), ) @@ -551,7 +553,12 @@ def cc_external_rule_impl(ctx, attrs): "##mkdirs## $$EXT_BUILD_DEPS$$", ] + _print_env() + _copy_deps_and_tools(inputs) + [ "cd $$BUILD_TMPDIR$$", - ] + attrs.create_configure_script(ConfigureParameters(ctx = ctx, attrs = attrs, inputs = inputs)) + postfix_script + validation_script + [ + ] + attrs.create_configure_script(ConfigureParameters( + ctx = ctx, + attrs = attrs, + inputs = inputs, + outputs = outputs, + )) + postfix_script + validation_script + [ # replace references to the root directory when building ($BUILD_TMPDIR) # and the root where the dependencies were installed ($EXT_BUILD_DEPS) # for the results which are in $INSTALLDIR (with placeholder) @@ -679,6 +686,22 @@ def cc_external_rule_impl(ctx, attrs): [externally_built], transitive = _get_transitive_artifacts(attrs.deps), )), + ForeignCcRuntimeExecutableInfo( + binaries = { + attrs.out_binaries[i]: outputs.out_binary_files[i] + for i in range(len(attrs.out_binaries)) + }, + runtime_files = depset(direct = ( + ([outputs.out_include_dir] if outputs.out_include_dir else []) + + outputs.libraries.shared_libraries + + outputs.data_dirs + + outputs.data_files + ), transitive = [ + dep[ForeignCcRuntimeExecutableInfo].runtime_files + for dep in attrs.deps + if ForeignCcRuntimeExecutableInfo in dep + ]), + ), CcInfo( compilation_context = out_cc_info.compilation_context, linking_context = out_cc_info.linking_context, diff --git a/foreign_cc/private/make_env_vars.bzl b/foreign_cc/private/make_env_vars.bzl index 54494a804..164cc2a18 100644 --- a/foreign_cc/private/make_env_vars.bzl +++ b/foreign_cc/private/make_env_vars.bzl @@ -1,5 +1,10 @@ """Helper methods to assemble make env variables from Bazel information.""" +load( + ":cc_toolchain_util.bzl", + "escape_loader_tokens_for_make", + "escape_loader_tokens_for_shell", +) load(":flag_utils.bzl", "absolutize_path_for_build_root", _join_flags_list = "join_flags_list") load(":framework.bzl", "get_foreign_cc_dep") @@ -12,7 +17,8 @@ def get_make_env_vars( deps, inputs, is_msvc, - make_commands): + make_commands, + expansion_context = "shell"): vars = _get_make_variables(workspace_name, tools, flags, user_vars, make_commands) deps_flags = _define_deps_flags(deps, inputs, is_msvc) @@ -38,8 +44,7 @@ def get_make_env_vars( else: vars["CPPFLAGS"] = deps_flags.flags - return " ".join(["{}=\"{}\"" - .format(key, _join_flags_list(workspace_name, vars[key])) for key in vars]) + return _format_vars(workspace_name, vars, expansion_context) # buildifier: disable=function-docstring def get_ldflags_make_vars( @@ -58,8 +63,7 @@ def get_ldflags_make_vars( for key in vars.keys(): vars[key] = vars[key] + deps_flags.libs - return " ".join(["{}=\"{}\"" - .format(key, _join_flags_list(workspace_name, vars[key])) for key in vars]) + return _format_vars(workspace_name, vars, "make") def _define_deps_flags(deps, inputs, is_msvc): # It is very important to keep the order for the linker => put them into list @@ -196,5 +200,10 @@ def _merge_env_vars(flags, make_flags, user_env_vars): vars[flag] = toolchain_flags + user_flags return vars +def _format_vars(workspace_name, vars, expansion_context): + escape_loader_tokens = escape_loader_tokens_for_make if expansion_context == "make" else escape_loader_tokens_for_shell + return " ".join(["{}=\"{}\"" + .format(key, escape_loader_tokens(_join_flags_list(workspace_name, vars[key]))) for key in vars]) + def _nmake_in_make_commands(make_commands): return make_commands and "nmake.exe" in make_commands[0] diff --git a/foreign_cc/private/make_script.bzl b/foreign_cc/private/make_script.bzl index a16345018..361e0a2d8 100644 --- a/foreign_cc/private/make_script.bzl +++ b/foreign_cc/private/make_script.bzl @@ -41,7 +41,17 @@ def create_make_script( install_prefix = make_install_prefix, )) - configure_vars = get_make_env_vars(workspace_name, tools, flags, env_vars, deps, inputs, is_msvc, make_commands) + configure_vars = get_make_env_vars( + workspace_name, + tools, + flags, + env_vars, + deps, + inputs, + is_msvc, + make_commands, + expansion_context = "make", + ) script.extend(["{env_vars} {command}".format( env_vars = configure_vars, diff --git a/foreign_cc/private/runtime_executable_wrapper.sh.tpl b/foreign_cc/private/runtime_executable_wrapper.sh.tpl new file mode 100644 index 000000000..9f4e85bc1 --- /dev/null +++ b/foreign_cc/private/runtime_executable_wrapper.sh.tpl @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +# --- begin runfiles.bash initialization v3 --- +# Copy-pasted from the Bazel Bash runfiles library v3. +set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash + +# This wrapper needs to resolve its selected foreign_cc binary from its own +# runfiles. When this wrapper is invoked by another Bazel-built tool, the parent +# may export RUNFILES_DIR or RUNFILES_MANIFEST_FILE for the parent's runfiles +# tree. Prefer the runfiles tree or manifest adjacent to $0 before sourcing +# runfiles.bash. +# +# For example, a parent tool may invoke this wrapper with +# RUNFILES_DIR=.../parent_tool.runfiles. If we keep that inherited value, +# rlocation will look for the selected binary in the parent tool's runfiles +# instead of this wrapper's runfiles. +if [[ -d "$0.runfiles" ]]; then + export RUNFILES_DIR="$0.runfiles" + unset RUNFILES_MANIFEST_FILE +elif [[ -f "$0.runfiles_manifest" ]]; then + export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest" + unset RUNFILES_DIR +elif [[ -f "$0.exe.runfiles_manifest" ]]; then + export RUNFILES_MANIFEST_FILE="$0.exe.runfiles_manifest" + unset RUNFILES_DIR +fi + +# shellcheck disable=SC1090 +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \ + source "$0.runfiles/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e +# --- end runfiles.bash initialization v3 --- + +runfiles_export_envvars + +binary="" +for candidate in %{binary_runfile_paths}; do + candidate_binary="$(rlocation "$candidate" 2>/dev/null || true)" + if [[ -n "$candidate_binary" && -x "$candidate_binary" ]]; then + binary="$candidate_binary" + break + fi +done + +if [[ -z "$binary" ]]; then + printf '%s\n' %{failure_message} >&2 + exit 1 +fi + +exec "$binary" "$@" diff --git a/foreign_cc/private/runtime_library_search_directories.bzl b/foreign_cc/private/runtime_library_search_directories.bzl new file mode 100644 index 000000000..108335c7c --- /dev/null +++ b/foreign_cc/private/runtime_library_search_directories.bzl @@ -0,0 +1,209 @@ +"""Policy facade for runtime library search path derivation.""" + +load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") +load(":runtime_search_paths.bzl", "derive_runtime_library_search_directories") + +RUNTIME_LIBRARY_SEARCH_DIRECTORY_ATTRIBUTES = { + "additional_dynamic_runtime_library_search_origins": attr.string_list( + doc = ( + "Additional install-tree-relative origins for shared-library " + + "link actions. Values are relative to this rule's install " + + "directory and should not include lib_name or the rule name. " + + "For each origin, runtime library search directories are derived " + + "so shared libraries loaded from that origin can find shared " + + "libraries from deps, dynamic_deps, and this rule's own declared " + + "shared-library outputs." + ), + mandatory = False, + default = [], + ), + "additional_executable_runtime_library_search_origins": attr.string_list( + doc = ( + "Additional install-tree-relative origins for executable link " + + "actions. Values are relative to this rule's install directory " + + "and should not include lib_name or the rule name. For each " + + "origin, runtime library search directories are derived so " + + "executables loaded from that origin can find shared libraries " + + "from deps, dynamic_deps, and this rule's own declared " + + "shared-library outputs." + ), + mandatory = False, + default = [], + ), + "runtime_library_search_directories": attr.string( + doc = ( + "Controls whether this target derives runtime library search " + + "directories. Use 'auto' to follow the global " + + "@rules_foreign_cc//foreign_cc/settings:runtime_library_search_directories " + + "build setting, 'enabled' to force it on for this target, or " + + "'disabled' to force it off. Global enablement is a strict Unix " + + "conformance switch: Unix targets that declare shared-library " + + "outputs must support the rule-specific shared-link flag hook or " + + "set runtime_library_search_directories = 'disabled'. This is " + + "not supported for Windows C++ toolchains; explicit per-target " + + "'enabled' fails on Windows, while 'auto' with global enablement " + + "is ignored on Windows. On macOS, derived runtime search paths also " + + "require compatible Mach-O @rpath install names and load " + + "commands. This feature passes runtime search directories to " + + "link actions, but does not rewrite installed dylib " + + "IDs or load commands after the upstream install step. Upstream " + + "builds may need to emit or preserve @rpath/... install names." + ), + mandatory = False, + values = ["auto", "enabled", "disabled"], + default = "auto", + ), + "_runtime_library_search_directories": attr.label( + default = Label("//foreign_cc/settings:runtime_library_search_directories"), + providers = [BuildSettingInfo], + ), +} + +# Policy helpers. + +# Returns true when the target explicitly forces runtime search on. +def _runtime_library_search_directories_explicitly_enabled(ctx): + return getattr(ctx.attr, "runtime_library_search_directories", "disabled") == "enabled" + +def _runtime_library_search_directories_requested(ctx): + value = getattr(ctx.attr, "runtime_library_search_directories", "disabled") + if value == "enabled": + return True + if value == "disabled": + return False + + setting = getattr(ctx.attr, "_runtime_library_search_directories", None) + return setting[BuildSettingInfo].value == "enabled" + +def runtime_library_search_directories_enabled(ctx, is_windows): + """Returns true when runtime search is effectively enabled. + + Runtime search can be requested explicitly by the target or inherited from + the global build setting. On Windows, explicit target opt-in fails because + runtime search is unsupported, while inherited global enablement through + `auto` is treated as a no-op. + + Args: + ctx: Rule context. + is_windows: Whether the target C++ toolchain is for Windows. + + Returns: + True when runtime search should be derived and passed to link variables. + """ + + if is_windows: + if _runtime_library_search_directories_explicitly_enabled(ctx): + fail(( + "ERROR: {} sets runtime_library_search_directories = " + + "\"enabled\", but runtime library search directories are not " + + "supported on Windows. Set runtime_library_search_directories " + + "= \"disabled\" for this target, or leave it as \"auto\" so " + + "global Unix conformance enablement is ignored on Windows." + ).format(ctx.label)) + return False + + return _runtime_library_search_directories_requested(ctx) + +def enforce_runtime_search_shared_ldflags_attr( + ctx, + runtime_search_enabled, + cxx_linker_shared, + hook_attr_name): + """Fails if runtime enabled targets cannot receive shared linker flags. + + Rules that declare `out_shared_libs` need a rule-specific hook for + shared-library linker flags whenever runtime search is enabled. Without that + hook, the derived rpaths for the produced shared libraries cannot be passed + to the upstream build system's shared-library link actions. + + Args: + ctx: Rule context. + runtime_search_enabled: Whether runtime search is effectively enabled for + this target platform. + cxx_linker_shared: Shared-library linker flags derived from the target C++ + toolchain. + hook_attr_name: Name of the rule attr that forwards shared-library linker + flags to the upstream build system, such as `shared_ldflags_vars` or + `shared_ldflags_option`. + """ + + if ( + runtime_search_enabled and + ctx.attr.out_shared_libs and + cxx_linker_shared and + not getattr(ctx.attr, hook_attr_name) + ): + fail(( + "ERROR: {} enables runtime_library_search_directories and " + + "declares shared-library outputs via out_shared_libs = {}, but " + + "{} is not set. The shared-library runtime search linker flags " + + "cannot be propagated to upstream shared-library link actions. " + + "Set {} to the upstream shared-link flag hook, or opt out with " + + "runtime_library_search_directories = \"disabled\"." + ).format( + ctx.label, + ctx.attr.out_shared_libs, + hook_attr_name, + hook_attr_name, + )) + +def _declared_runtime_attrs(ctx): + runtime_attrs = [] + for attr_name in [ + "additional_dynamic_runtime_library_search_origins", + "additional_executable_runtime_library_search_origins", + ]: + if getattr(ctx.attr, attr_name, []): + runtime_attrs.append(attr_name) + return runtime_attrs + +def _has_outputs_for_runtime_path(outputs): + return bool( + outputs.libraries.shared_libraries or + outputs.out_binary_files or + outputs.data_dirs or + outputs.data_files, + ) + +def runtime_library_search_directories(ctx, outputs): + """Returns runtime library search directories for link actions. + + Args: + ctx: Rule context. + outputs: Framework-declared outputs. Shared libraries and binaries provide + the default runtime origins; one of them is also used to recover + INSTALLDIR in `File.short_path` space. + + Returns: + A struct with `shared_dirs` and `executable_dirs` depset fields. Each field + contains runtime library search directories for that link action kind, or + None when runtime library search directory derivation is disabled. + """ + d_attrs = _declared_runtime_attrs(ctx) + + d_attrs = _declared_runtime_attrs(ctx) + if not _runtime_library_search_directories_requested(ctx): + if d_attrs: + fail(( + "FAIL: {} sets runtime_library_search attrs ({}) but " + + "runtime_library_search_directories is disabled." + ).format(ctx.label, ", ".join(d_attrs))) + return struct(shared_dirs = None, executable_dirs = None) + + # If no outputs are expected, we don't need to worry about + # runtime library search directories. + if outputs == None: + return struct(shared_dirs = None, executable_dirs = None) + + if d_attrs and not _has_outputs_for_runtime_path(outputs): + fail(( + "{} sets runtime_library_search attrs ({}) but none of the outputs " + + "(out_shared_libs, out_binaries, out_data_dirs, or out_data_files) " + + "needing runtime path is declared." + ).format(ctx.label, ", ".join(d_attrs))) + + return derive_runtime_library_search_directories(ctx, outputs) + +export_for_test = struct( + runtime_library_search_directories_requested = _runtime_library_search_directories_requested, +) diff --git a/foreign_cc/private/runtime_search_paths.bzl b/foreign_cc/private/runtime_search_paths.bzl new file mode 100644 index 000000000..4a197457f --- /dev/null +++ b/foreign_cc/private/runtime_search_paths.bzl @@ -0,0 +1,382 @@ +"""Derives loader-relative runtime library search paths.""" + +load("@bazel_skylib//lib:paths.bzl", "paths") +load("@rules_cc//cc:defs.bzl", "CcInfo") +load("@rules_cc//cc/common:cc_shared_library_info.bzl", "CcSharedLibraryInfo") + +# See the doc-string for `derive_runtime_library_search_directories` for a mental +# model of how to read this file. + +def _path_segments(path): + normalized = paths.normalize(path) + if normalized == ".": + return [] + return [segment for segment in normalized.split("/") if segment] + +# `File.short_path` for external dependencies appears as `../repo/...` because +# it is calculated in runfiles space, where external dependencies are adjacent to +# the main repo: +# +# app.runfiles/ +# _main/ +# myapp/app +# zlib/ +# lib/libz.so +# +# From execroot/output coordinates, the same external dependency appears under +# `external/repo/...`: +# +# /execroot/_main/ +# bazel-out/ +# k8-fastbuild/ +# bin/ +# myproj/ +# myapp/app +# external/ +# zlib/lib/libz.so +# +# The binary inside the runfiles is a symlink to the file in execroot. When +# executing a binary through a symlink the $ORIGIN on in its RUNPATH evaluates +# to the path of the real file. Therefore, for it to find the dependent shared +# libs, we need the path to be in execroot coordinates. On Darwin, @loader_path +# in LC_RPATH behaves the same way. Hence converting `../` to `external/`. +def _runtime_search_path(path): + segments = _path_segments(path) + if len(segments) >= 2 and segments[0] == "..": + return "external/" + "/".join(segments[1:]) + return "/".join(segments) + +def _common_prefix_length(left, right): + max_common_segment_count = len(left) + if len(right) < max_common_segment_count: + max_common_segment_count = len(right) + + for index in range(max_common_segment_count): + if left[index] != right[index]: + return index + + return max_common_segment_count + +# Return the path fragment appended after `$ORIGIN` or `@loader_path`. For +# example, from "pkg/python/bin" to "pkg/python/lib" this returns "../lib". +def _relative_path(from_path, to_path): + from_segments = _path_segments(_runtime_search_path(from_path)) + to_segments = _path_segments(_runtime_search_path(to_path)) + common_segment_count = _common_prefix_length(from_segments, to_segments) + + # Drop the shared prefix, then walk up from the origin and down to the target. + relative_segments = ( + [".."] * (len(from_segments) - common_segment_count) + + to_segments[common_segment_count:] + ) + return "/".join(relative_segments) if relative_segments else "." + +def _dedupe_strings(strings): + # Preserve first-seen order because runtime search directory order affects + # which matching soname the dynamic loader resolves first. + seen = {} + deduped = [] + for string in strings: + if string not in seen: + seen[string] = True + deduped.append(string) + return deduped + +def _dynamic_libraries(linker_input): + dynamic_libraries = [] + for library in linker_input.libraries: + if library.dynamic_library: + dynamic_libraries.append(library.dynamic_library) + if library.resolved_symlink_dynamic_library: + dynamic_libraries.append(library.resolved_symlink_dynamic_library) + return dynamic_libraries + +def _dynamic_libraries_from_dep(dep): + dynamic_libraries = [] + if CcInfo in dep: + for linker_input in dep[CcInfo].linking_context.linker_inputs.to_list(): + dynamic_libraries.extend(_dynamic_libraries(linker_input)) + + if CcSharedLibraryInfo in dep: + cc_shared_library_info = dep[CcSharedLibraryInfo] + dynamic_libraries.extend(_dynamic_libraries(cc_shared_library_info.linker_input)) + for dynamic_dep in cc_shared_library_info.dynamic_deps.to_list(): + dynamic_libraries.extend(_dynamic_libraries(dynamic_dep.linker_input)) + + return dynamic_libraries + +def _dynamic_library_dirs_from_deps(ctx): + dynamic_library_dirs = [] + for dep in getattr(ctx.attr, "deps", []) + getattr(ctx.attr, "dynamic_deps", []): + for dynamic_library in _dynamic_libraries_from_dep(dep): + dynamic_library_dirs.append(paths.dirname(dynamic_library.short_path)) + return _dedupe_strings(dynamic_library_dirs) + +# Declared output files provide the default origins. A binary at +# "pkg/python/bin/python3.10" contributes "pkg/python/bin"; a shared library at +# "pkg/python/lib/libpython.so" contributes "pkg/python/lib". +def _dirs_from_files(files): + return [ + paths.dirname(file.short_path) + for file in files + ] + +# Build the install-relative path used when the framework declared an output. +# This is the suffix we expect to strip from the output's `File.short_path`. +def _install_path(dir_, file): + dir_ = dir_.strip("/") + return paths.join(dir_, file) if dir_ else file + +# Recover the foreign_cc INSTALLDIR in `File.short_path` space. For example, +# "pkg/python/lib/libpython.so" minus "lib/libpython.so" gives "pkg/python". +def _derive_installdir(output_file, install_path): + install_path = install_path.lstrip("/") + suffix = "/" + install_path + short_path = output_file.short_path + + if short_path.endswith(suffix): + return short_path[:-len(suffix)] + + fail("Output {} does not end with install-relative path {}".format( + short_path, + install_path, + )) + +def _installdir_from_outputs(ctx, outputs): + if outputs.libraries.shared_libraries: + return _derive_installdir( + outputs.libraries.shared_libraries[0], + _install_path(ctx.attr.out_lib_dir, ctx.attr.out_shared_libs[0]), + ) + + if outputs.out_binary_files: + return _derive_installdir( + outputs.out_binary_files[0], + _install_path(ctx.attr.out_bin_dir, ctx.attr.out_binaries[0]), + ) + + if outputs.data_dirs: + return _derive_installdir( + outputs.data_dirs[0], + ctx.attr.out_data_dirs[0].lstrip("/"), + ) + + if outputs.data_files: + return _derive_installdir( + outputs.data_files[0], + ctx.attr.out_data_files[0].lstrip("/"), + ) + + return None + +# Users specify extra origins relative to INSTALLDIR, for example +# "lib/python3.10/lib-dynload". Expand those into the same `File.short_path` +# space as declared outputs. I.e. append INSTALLDIR +def _origins_from_install_tree(installdir, install_tree_origins): + origins = [] + for install_tree_origin in install_tree_origins: + origin = install_tree_origin.lstrip("/") + origins.append(paths.join( + installdir, + origin, + ) if origin else installdir) + return origins + +# Return a list of short_path directories of where the rule's outputs (shared +# lib/binaries) may be at runtime. This includes any user defined origins in +# the same format. +# +# Examples: +# shared lib "pkg/python/lib/libpython.so" contributes "pkg/python/lib" +# binary "pkg/python/bin/python3.10" contributes "pkg/python/bin" +# additional origin "lib/python3.10/lib-dynload" under INSTALLDIR +# "pkg/python" contributes "pkg/python/lib/python3.10/lib-dynload" +# +# so this returns: +# [ +# "pkg/python/lib", +# "pkg/python/bin", +# "pkg/python/lib/python3.10/lib-dynload", +# ] +def _evaluate_origins(installdir, output_files, additional_origins): + return _dedupe_strings( + _dirs_from_files(output_files) + + _origins_from_install_tree( + installdir, + additional_origins, + ), + ) + +def _search_directories(origins, library_dirs): + directories = [] + for origin in origins: + for library_dir in library_dirs: + directories.append(_relative_path(origin, library_dir)) + return directories + +# Return the sibling-solib search entry for a dependent dynamic library dir. +# Bazel often links against solib symlinks instead of real output paths, and a +# sibling entry lets a library loaded from one solib artifact directory find a +# dependency in another. +# +# For example, for a dep_library_dir of: +# "_solib_k8/_Uthirdparty_Szlib" +# +# This will return the runtime search path of relative to _solib_k8: +# "../_Uthirdparty_Szlib" +# +# Inputs without a solib root and artifact directory return None: +# "pkg/zlib/lib" -> None +# "_solib_k8" -> None +# +# This helper assumes that the path of the library (the ones we are building in +# the rule) needing the solib sibling search directory is exactly 1 directory +# deep under the solib root. e.g. +# "_solib_k8/_Uthirdparty_Szlib/libz.so" or +# "_solib_k8/_Uthirdparty_Sopenssl__Slib/libcrypto.so" +# +# While this is true for most scenarios, edge cases do exist. One example of +# this is to set "dynamic_library_symlink_path" when using +# cc_common.create_library_to_link. However, it is +# not possible to account for this without knowing the solib path of library +# we are building in advance. So this is our best effort. +def _solib_sibling_search_directory(dep_library_dir): + segments = _path_segments(dep_library_dir) + if len(segments) < 2 or not segments[0].startswith("_solib_"): + return None + + artifact_dir_under_solib = "/".join(segments[1:]) + if not artifact_dir_under_solib: + return None + + return "../" + artifact_dir_under_solib + +def _solib_sibling_search_directories(dep_library_dirs): + directories = [] + for dep_library_dir in dep_library_dirs: + solib_sibling_directory = _solib_sibling_search_directory(dep_library_dir) + if solib_sibling_directory: + directories.append(solib_sibling_directory) + return directories + +# Runtime search directories are derived from the places the rule's outputs may +# be loaded from at runtime. +# +# For each group of output_files, we build a set of origins: +# 1. default origins from declared outputs, such as "pkg/python/bin" or +# "pkg/python/lib", where "python" is the lib_name. +# 2. user-provided origins under INSTALLDIR, such as +# "lib/python3.10/lib-dynload" +# +# From each origin, we add relative search paths to: +# 1. the rule's own declared shared-library directories +# 2. dynamic-library directories exposed by deps and dynamic_deps +# +# Those relative paths become entries after `$ORIGIN` on ELF or `@loader_path` +# on Darwin. +def _runtime_library_search_directories_for_outputs( + output_files, + additional_origins, + installdir, + self_library_dirs, + dep_library_dirs): + origins = _evaluate_origins( + installdir, + output_files, + additional_origins, + ) + + directories = [] + directories.extend(_search_directories(origins, self_library_dirs)) + directories.extend(_search_directories( + origins, + dep_library_dirs, + )) + directories.extend(_solib_sibling_search_directories(dep_library_dirs)) + return _dedupe_strings(directories) + +def derive_runtime_library_search_directories(ctx, outputs): + """Derives runtime library search directories for an enabled target. + + The caller is responsible for policy checks before calling this helper: + runtime search should already be enabled, unsupported platforms should + already be rejected, outputs must be available, and additional runtime + origins must be rejected when no declared output can recover INSTALLDIR in + `File.short_path` space. + + This derives the runtime search path for declared executables and shared + libraries of the foreign_cc target by calculating the relative path from + those artifacts to the target's own shared libraries and its dep's and + dynamic_deps's shared libraries. + + For each place an output can be *loaded from* (an "origin" / FROM), write a + relative path to each place its libraries live (a "library dir" / TO). That + relative string becomes an `$ORIGIN`-relative RPATH entry (`$ORIGIN` on ELF, + `@loader_path` on Darwin), e.g. a binary in `bin/` needing a lib in `lib/` + yields `../lib`. + + Two passes share the same TO lists but differ only in their FROM: + + pass FROM (origins) TO (library dirs) + ---------- ----------------- ---------------------------- + shared this rule's libs own libs + deps' libs + executable this rule's bins own libs + deps' libs + + If any `additional_*_runtime_library_search_origins` are declared, they are + added to the respective `FROM (origins)`. + + An empty FROM (e.g. no binaries) yields no paths for that pass. Everything + else here is edge-case plumbing layered on this core: + - `additional_origins` adds extra FROMs + - `_solib_sibling_*` handles Bazel's `_solib_*` symlink dirs + - `../` -> `external/` reconciles runfiles vs execroot for external repos + - INSTALLDIR recovery backs the install root out of a File.short_path. + + Args: + ctx: Rule context. + outputs: Framework-declared outputs. + + Returns: + A struct with `shared_dirs` and `executable_dirs` depset fields containing + loader-relative runtime library search directories. + """ + + shared_files = outputs.libraries.shared_libraries + binary_files = outputs.out_binary_files + + self_library_dirs = _dirs_from_files(shared_files) + dep_library_dirs = _dynamic_library_dirs_from_deps(ctx) + + additional_dynamic_origins = getattr(ctx.attr, "additional_dynamic_runtime_library_search_origins", []) + additional_executable_origins = getattr(ctx.attr, "additional_executable_runtime_library_search_origins", []) + + installdir = None + if additional_dynamic_origins or additional_executable_origins: + installdir = _installdir_from_outputs(ctx, outputs) + + shared_rpaths = _runtime_library_search_directories_for_outputs( + shared_files, + additional_dynamic_origins, + installdir, + self_library_dirs, + dep_library_dirs, + ) + + executable_rpaths = _runtime_library_search_directories_for_outputs( + binary_files, + additional_executable_origins, + installdir, + self_library_dirs, + dep_library_dirs, + ) + + return struct( + shared_dirs = depset(shared_rpaths), + executable_dirs = depset(executable_rpaths), + ) + +export_for_test = struct( + installdir_from_outputs = _installdir_from_outputs, + search_directories = _search_directories, + solib_sibling_search_directories = _solib_sibling_search_directories, +) diff --git a/foreign_cc/private/transitions.bzl b/foreign_cc/private/transitions.bzl index 218adcf27..365550380 100644 --- a/foreign_cc/private/transitions.bzl +++ b/foreign_cc/private/transitions.bzl @@ -1,7 +1,7 @@ """This file contains rules for configuration transitions""" load("@rules_cc//cc:defs.bzl", "CcInfo") -load("//foreign_cc:providers.bzl", "ForeignCcDepsInfo") +load("//foreign_cc:providers.bzl", "ForeignCcDepsInfo", "ForeignCcRuntimeExecutableInfo") def _extra_toolchains_transition_impl(settings, attrs): t = getattr(attrs, "extra_toolchain", None) @@ -18,12 +18,15 @@ _extra_toolchains_transition = transition( def _extra_toolchains_transitioned_foreign_cc_target_impl(ctx): # Return the providers from the transitioned foreign_cc target - return [ + providers = [ ctx.attr.target[DefaultInfo], ctx.attr.target[CcInfo], ctx.attr.target[ForeignCcDepsInfo], ctx.attr.target[OutputGroupInfo], ] + if ForeignCcRuntimeExecutableInfo in ctx.attr.target: + providers.append(ctx.attr.target[ForeignCcRuntimeExecutableInfo]) + return providers extra_toolchains_transitioned_foreign_cc_target = rule( doc = "A rule for adding an extra toolchain to consider when building the given target", diff --git a/foreign_cc/providers.bzl b/foreign_cc/providers.bzl index 2de172f14..ab1b25bc3 100644 --- a/foreign_cc/providers.bzl +++ b/foreign_cc/providers.bzl @@ -23,3 +23,11 @@ Instances of ForeignCcArtifactInfo are encapsulated in a depset [ForeignCcDepsIn "lib_dir_name": "Lib directory, relative to install directory", }, ) + +ForeignCcRuntimeExecutableInfo = provider( + doc = """Provider exposing declared foreign_cc runtime files for executable adapters.""", + fields = { + "binaries": "Dictionary mapping exact out_binaries entries to declared binary Files.", + "runtime_files": "Depset of declared foreign_cc outputs needed at runtime.", + }, +) diff --git a/foreign_cc/runtime_executable.bzl b/foreign_cc/runtime_executable.bzl new file mode 100644 index 000000000..6ae908f92 --- /dev/null +++ b/foreign_cc/runtime_executable.bzl @@ -0,0 +1,163 @@ +"""Executable adapter for binaries produced by foreign_cc targets.""" + +load("@bazel_skylib//lib:shell.bzl", "shell") +load("@rules_shell//shell:sh_binary.bzl", "sh_binary") +load("//foreign_cc:providers.bzl", "ForeignCcRuntimeExecutableInfo") + +_PRIVATE_TARGET_ATTRS = [ + "compatible_with", + "exec_compatible_with", + "exec_group_compatible_with", + "exec_properties", + "restricted_to", + "tags", + "target_compatible_with", + "testonly", +] + +def _runtime_executable_wrapper_impl(ctx): + runtime_info = ctx.attr.foreign_cc_target[ForeignCcRuntimeExecutableInfo] + binary = ctx.attr.binary + if binary not in runtime_info.binaries: + fail("runtime_executable binary '{}' was not found in {} out_binaries: {}".format( + binary, + ctx.attr.foreign_cc_target.label, + ", ".join(sorted(runtime_info.binaries.keys())), + )) + + selected_binary = runtime_info.binaries[binary] + executable = ctx.actions.declare_file(ctx.label.name + ".sh") + wrapper_paths = _runfile_paths(ctx, selected_binary) + ctx.actions.expand_template( + output = executable, + template = ctx.file._wrapper_template, + substitutions = { + "%{binary_runfile_paths}": " ".join([shell.quote(path) for path in wrapper_paths]), + "%{failure_message}": shell.quote( + "runtime executable is not executable: " + ", ".join(wrapper_paths), + ), + }, + is_executable = True, + ) + + runfiles = ctx.runfiles( + files = [selected_binary], + transitive_files = runtime_info.runtime_files, + ) + runfiles = runfiles.merge(ctx.attr.foreign_cc_target[DefaultInfo].default_runfiles) + runfiles = runfiles.merge(ctx.attr._runfiles[DefaultInfo].default_runfiles) + + return [DefaultInfo( + files = depset([executable]), + runfiles = runfiles, + )] + +# Build the runfiles lookup keys the wrapper should try for the selected binary. +# +# File.short_path is the path fragment for declared outputs, but it is not +# always the exact key accepted by runfiles.bash rlocation. In the workspace +# that owns the adapter target, runfiles can include the workspace name as the +# first path segment, e.g. "_main/pkg/tool/bin/app", while File.short_path is +# "pkg/tool/bin/app". For external repositories, File.short_path may start +# with "../repo/", and the runfiles key drops that leading "../". +# +# Example: +# file.short_path = "pkg/python/bin/python3.12" +# ctx.workspace_name = "_main" +# candidates = [ +# "_main/pkg/python/bin/python3.12", +# "pkg/python/bin/python3.12", +# ] +def _runfile_paths(ctx, file): + if file.short_path.startswith("../"): + return [file.short_path[3:]] + + if ctx.workspace_name: + return [ + ctx.workspace_name + "/" + file.short_path, + file.short_path, + ] + return [file.short_path] + +_runtime_executable_wrapper = rule( + implementation = _runtime_executable_wrapper_impl, + attrs = { + "binary": attr.string( + mandatory = True, + doc = "Exact configured entry from foreign_cc_target's out_binaries to execute.", + ), + "foreign_cc_target": attr.label( + mandatory = True, + doc = "foreign_cc target that declares the selected binary in out_binaries.", + providers = [ForeignCcRuntimeExecutableInfo], + ), + "_runfiles": attr.label( + default = "@bazel_tools//tools/bash/runfiles", + ), + "_wrapper_template": attr.label( + allow_single_file = True, + default = "//foreign_cc/private:runtime_executable_wrapper.sh.tpl", + ), + }, +) + +# The public sh_binary accepts attrs like data, deps, args, and env that do +# not belong on the generated wrapper rule. Forward only attrs that affect +# test-only checks, compatibility, or wrapper action execution behavior. +def _private_target_kwargs(kwargs): + return { + key: kwargs[key] + for key in _PRIVATE_TARGET_ATTRS + if key in kwargs + } + +def runtime_executable(name, binary, foreign_cc_target, **kwargs): + """Turns a selected foreign_cc binary output into an executable Bazel target. + + This adapter is separate from the producing foreign_cc rule because + foreign_cc outputs are not always expected to contain binaries, so the + producing rule cannot always expose an executable. + + The selected binary must be declared by the producing target's out_binaries + attribute. If that binary depends directly or transitively on + foreign_cc-produced shared libraries, the producing foreign_cc target may + also need runtime_library_search_directories = "enabled". + + runtime_executable is a more Bazel-native form of runnable_binary because it + exposes the executable through DefaultInfo.files_to_run for downstream + consumers. Its API is intentionally similar to runnable_binary, but it does + not completely replace runnable_binary while + runtime_library_search_directories defaults to "disabled". + + The adapter creates a runfiles-aware shell wrapper that resolves the + selected binary through Bazel runfiles, exports runfiles environment + variables, and reports a clear error if the binary cannot be located or + executed. + + Args: + name: Name of the executable target. + binary: Exact configured entry from foreign_cc_target's out_binaries to execute. + foreign_cc_target: foreign_cc target that declares the selected binary in + out_binaries. + **kwargs: Common rule attributes forwarded to the public sh_binary target. + Compatibility-related attributes are also applied to the generated + wrapper target. + """ + wrapper_name = name + "_wrapper" + wrapper_kwargs = _private_target_kwargs(kwargs) + wrapper_kwargs["tags"] = wrapper_kwargs.get("tags", []) + ["manual"] + + _runtime_executable_wrapper( + name = wrapper_name, + binary = binary, + foreign_cc_target = foreign_cc_target, + **wrapper_kwargs + ) + + sh_binary( + name = name, + srcs = [":" + wrapper_name], + data = [":" + wrapper_name], + deps = ["@bazel_tools//tools/bash/runfiles"], + **kwargs + ) diff --git a/foreign_cc/settings/BUILD.bazel b/foreign_cc/settings/BUILD.bazel index 8535d0bc0..37d20bae8 100644 --- a/foreign_cc/settings/BUILD.bazel +++ b/foreign_cc/settings/BUILD.bazel @@ -1,4 +1,4 @@ -load("@bazel_skylib//rules:common_settings.bzl", "bool_flag") +load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "string_flag") load( "//foreign_cc/private:resource_sets.bzl", _create_resource_set_settings = "create_resource_set_settings", @@ -23,6 +23,16 @@ bool_flag( visibility = ["//visibility:public"], ) +string_flag( + name = "runtime_library_search_directories", + build_setting_default = "disabled", + values = [ + "enabled", + "disabled", + ], + visibility = ["//visibility:public"], +) + # `bazel run //foreign_cc/settings` prints every public build setting in # bazelrc form so users can copy them verbatim into their own bazelrc. _settings_script( @@ -38,5 +48,10 @@ _settings_script( "set_file_prefix_map_default", False, ), + ( + (2, 2), + "runtime_library_search_directories", + "disabled", + ), ], ) diff --git a/test/BUILD.bazel b/test/BUILD.bazel index b215a558b..65f713410 100644 --- a/test/BUILD.bazel +++ b/test/BUILD.bazel @@ -8,6 +8,8 @@ load(":make_env_vars_test.bzl", "make_env_vars_test_suite") load(":meson_text_tests.bzl", "meson_script_test_suite") load(":msbuild_text_tests.bzl", "msbuild_script_test_suite") load(":planner_test.bzl", "planner_test_suite") +load(":runtime_executable_test.bzl", "runtime_executable_test_suite") +load(":runtime_search_test.bzl", "runtime_library_search_directories_test_suite") load(":shell_script_helper_test_rule.bzl", "shell_script_helper_test_rule") load(":symlink_contents_to_dir_test_rule.bzl", "symlink_contents_to_dir_test_rule") load(":utils_test.bzl", "utils_test_suite") @@ -68,6 +70,21 @@ meson_script_test_suite() msbuild_script_test_suite() +runtime_library_search_directories_test_suite() + +runtime_executable_test_suite() + +sh_test( + name = "runtime_executable_shell_quoting_execution_test", + size = "small", + srcs = ["runtime_executable_shell_quoting_test.sh"], + args = ["$(rlocationpath :runtime_executable_shell_quoting_subject)"], + data = [ + ":runtime_executable_shell_quoting_subject", + "@bazel_tools//tools/bash/runfiles", + ], +) + shell_script_conversion_suite() make_env_vars_test_suite() diff --git a/test/cmake_text_tests.bzl b/test/cmake_text_tests.bzl index 085ad6875..e72eb0207 100644 --- a/test/cmake_text_tests.bzl +++ b/test/cmake_text_tests.bzl @@ -27,6 +27,38 @@ def _absolutize_test(ctx): return unittest.end(env) +def _join_flags_list_escapes_loader_tokens_for_shell_test(ctx): + env = unittest.begin(ctx) + + result = export_for_test.join_flags_list("ws", [ + "-Wl,-rpath,$ORIGIN/lib", + "-Wl,-rpath,$EXEC_ORIGIN/bin", + ]) + + asserts.equals( + env, + "-Wl,-rpath,\\$ORIGIN/lib -Wl,-rpath,\\$EXEC_ORIGIN/bin", + result, + ) + + return unittest.end(env) + +def _join_flags_list_preserves_escaped_loader_tokens_for_shell_test(ctx): + env = unittest.begin(ctx) + + result = export_for_test.join_flags_list("ws", [ + "-Wl,-rpath,\\$ORIGIN/lib", + "-Wl,-rpath,\\$EXEC_ORIGIN/bin", + ]) + + asserts.equals( + env, + "-Wl,-rpath,\\$ORIGIN/lib -Wl,-rpath,\\$EXEC_ORIGIN/bin", + result, + ) + + return unittest.end(env) + def _tail_extraction_test(ctx): env = unittest.begin(ctx) @@ -944,6 +976,8 @@ cmake -DCUSTOM_CACHE="YES" -DCMAKE_TOOLCHAIN_FILE="$$BUILD_TMPDIR$$/crosstool_ba return unittest.end(env) absolutize_test = unittest.make(_absolutize_test) +join_flags_list_escapes_loader_tokens_for_shell_test = unittest.make(_join_flags_list_escapes_loader_tokens_for_shell_test) +join_flags_list_preserves_escaped_loader_tokens_for_shell_test = unittest.make(_join_flags_list_preserves_escaped_loader_tokens_for_shell_test) tail_extraction_test = unittest.make(_tail_extraction_test) find_flag_value_test = unittest.make(_find_flag_value_test) fill_crossfile_from_toolchain_test = unittest.make(_fill_crossfile_from_toolchain_test) @@ -966,6 +1000,8 @@ def cmake_script_test_suite(): unittest.suite( "cmake_script_test_suite", partial.make(absolutize_test, size = "small"), + partial.make(join_flags_list_escapes_loader_tokens_for_shell_test, size = "small"), + partial.make(join_flags_list_preserves_escaped_loader_tokens_for_shell_test, size = "small"), partial.make(tail_extraction_test, size = "small"), partial.make(find_flag_value_test, size = "small"), partial.make(fill_crossfile_from_toolchain_test, size = "small"), diff --git a/test/make_env_vars_test.bzl b/test/make_env_vars_test.bzl index 7c520743a..35859206a 100644 --- a/test/make_env_vars_test.bzl +++ b/test/make_env_vars_test.bzl @@ -7,7 +7,7 @@ load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") load("//foreign_cc/private:cc_toolchain_util.bzl", "export_for_test") # buildifier: disable=bzl-visibility -load("//foreign_cc/private:make_env_vars.bzl", "get_ldflags_make_vars") +load("//foreign_cc/private:make_env_vars.bzl", "get_ldflags_make_vars", "get_make_env_vars") def _empty_inputs(): return struct( @@ -95,6 +95,150 @@ ldflags_vars_keep_link_categories_independent_test = unittest.make(_ldflags_vars dynamic_module_link_flags_strip_darwin_library_flags_test = unittest.make(_dynamic_module_link_flags_strip_darwin_library_flags_test) dynamic_module_link_flags_preserve_non_darwin_flags_test = unittest.make(_dynamic_module_link_flags_preserve_non_darwin_flags_test) +def _tools(): + return struct( + cc = "", + cxx = "", + cxx_linker_static = "", + ld = "", + ) + +def _flags(): + return struct( + assemble = [], + cc = [], + cxx = [], + cxx_linker_executable = [ + "-Wl,-rpath,$ORIGIN/lib", + "-Wl,-rpath,$EXEC_ORIGIN/bin", + ], + cxx_linker_static = [], + ) + +def _escaped_flags(): + return struct( + assemble = [], + cc = [], + cxx = [], + cxx_linker_executable = [ + "-Wl,-rpath,\\$ORIGIN/lib", + "-Wl,-rpath,\\$EXEC_ORIGIN/bin", + ], + cxx_linker_static = [], + ) + +def _inputs(): + return struct( + headers = [], + include_dirs = [], + libs = [], + ) + +def _get_make_env_vars_escapes_loader_tokens_for_shell_test(ctx): + env = unittest.begin(ctx) + + result = get_make_env_vars("workspace", _tools(), _flags(), {}, [], _inputs(), False, []) + + asserts.equals( + env, + "LDFLAGS=\"-Wl,-rpath,\\$ORIGIN/lib -Wl,-rpath,\\$EXEC_ORIGIN/bin\" RANLIB=\":\" CPPFLAGS=\"\"", + result, + ) + + return unittest.end(env) + +def _get_make_env_vars_preserves_escaped_loader_tokens_for_shell_test(ctx): + env = unittest.begin(ctx) + + result = get_make_env_vars("workspace", _tools(), _escaped_flags(), {}, [], _inputs(), False, []) + + asserts.equals( + env, + "LDFLAGS=\"-Wl,-rpath,\\$ORIGIN/lib -Wl,-rpath,\\$EXEC_ORIGIN/bin\" RANLIB=\":\" CPPFLAGS=\"\"", + result, + ) + + return unittest.end(env) + +def _get_make_env_vars_make_context_escapes_loader_tokens_for_make_test(ctx): + env = unittest.begin(ctx) + + result = get_make_env_vars( + "workspace", + _tools(), + _flags(), + {}, + [], + _inputs(), + False, + [], + expansion_context = "make", + ) + + asserts.equals( + env, + "LDFLAGS=\"-Wl,-rpath,\\\\$\\$ORIGIN/lib -Wl,-rpath,\\\\$\\$EXEC_ORIGIN/bin\" RANLIB=\":\" CPPFLAGS=\"\"", + result, + ) + + return unittest.end(env) + +def _get_make_env_vars_make_context_normalizes_escaped_loader_tokens_test(ctx): + env = unittest.begin(ctx) + + result = get_make_env_vars( + "workspace", + _tools(), + _escaped_flags(), + {}, + [], + _inputs(), + False, + [], + expansion_context = "make", + ) + + asserts.equals( + env, + "LDFLAGS=\"-Wl,-rpath,\\\\$\\$ORIGIN/lib -Wl,-rpath,\\\\$\\$EXEC_ORIGIN/bin\" RANLIB=\":\" CPPFLAGS=\"\"", + result, + ) + + return unittest.end(env) + +def _get_ldflags_make_vars_escapes_loader_tokens_for_make_test(ctx): + env = unittest.begin(ctx) + + result = get_ldflags_make_vars(["LDFLAGS"], [], [], "workspace", _flags(), {}, [], _inputs(), False) + + asserts.equals( + env, + "LDFLAGS=\"-Wl,-rpath,\\\\$\\$ORIGIN/lib -Wl,-rpath,\\\\$\\$EXEC_ORIGIN/bin\"", + result, + ) + + return unittest.end(env) + +def _get_ldflags_make_vars_normalizes_escaped_loader_tokens_for_make_test(ctx): + env = unittest.begin(ctx) + + result = get_ldflags_make_vars(["LDFLAGS"], [], [], "workspace", _escaped_flags(), {}, [], _inputs(), False) + + asserts.equals( + env, + "LDFLAGS=\"-Wl,-rpath,\\\\$\\$ORIGIN/lib -Wl,-rpath,\\\\$\\$EXEC_ORIGIN/bin\"", + result, + ) + + return unittest.end(env) + +get_make_env_vars_escapes_loader_tokens_for_shell_test = unittest.make(_get_make_env_vars_escapes_loader_tokens_for_shell_test) +get_make_env_vars_preserves_escaped_loader_tokens_for_shell_test = unittest.make(_get_make_env_vars_preserves_escaped_loader_tokens_for_shell_test) +get_make_env_vars_make_context_escapes_loader_tokens_for_make_test = unittest.make(_get_make_env_vars_make_context_escapes_loader_tokens_for_make_test) +get_make_env_vars_make_context_normalizes_escaped_loader_tokens_test = unittest.make(_get_make_env_vars_make_context_normalizes_escaped_loader_tokens_test) +get_ldflags_make_vars_escapes_loader_tokens_for_make_test = unittest.make(_get_ldflags_make_vars_escapes_loader_tokens_for_make_test) +get_ldflags_make_vars_normalizes_escaped_loader_tokens_for_make_test = unittest.make(_get_ldflags_make_vars_normalizes_escaped_loader_tokens_for_make_test) + def make_env_vars_test_suite(): unittest.suite( "make_env_vars_test_suite", @@ -102,4 +246,10 @@ def make_env_vars_test_suite(): partial.make(ldflags_vars_keep_link_categories_independent_test, size = "small"), partial.make(dynamic_module_link_flags_strip_darwin_library_flags_test, size = "small"), partial.make(dynamic_module_link_flags_preserve_non_darwin_flags_test, size = "small"), + partial.make(get_make_env_vars_escapes_loader_tokens_for_shell_test, size = "small"), + partial.make(get_make_env_vars_preserves_escaped_loader_tokens_for_shell_test, size = "small"), + partial.make(get_make_env_vars_make_context_escapes_loader_tokens_for_make_test, size = "small"), + partial.make(get_make_env_vars_make_context_normalizes_escaped_loader_tokens_test, size = "small"), + partial.make(get_ldflags_make_vars_escapes_loader_tokens_for_make_test, size = "small"), + partial.make(get_ldflags_make_vars_normalizes_escaped_loader_tokens_for_make_test, size = "small"), ) diff --git a/test/meson_text_tests.bzl b/test/meson_text_tests.bzl index aed431bf1..fa183f4f0 100644 --- a/test/meson_text_tests.bzl +++ b/test/meson_text_tests.bzl @@ -25,10 +25,46 @@ def _list_to_str_repr_test(ctx): return unittest.end(env) +def _join_flags_list_escapes_loader_tokens_for_shell_test(ctx): + env = unittest.begin(ctx) + + result = export_for_test.join_flags_list("ws", [ + "-Wl,-rpath,$ORIGIN/lib", + "-Wl,-rpath,$EXEC_ORIGIN/bin", + ]) + + asserts.equals( + env, + "-Wl,-rpath,\\$ORIGIN/lib -Wl,-rpath,\\$EXEC_ORIGIN/bin", + result, + ) + + return unittest.end(env) + +def _join_flags_list_preserves_escaped_loader_tokens_for_shell_test(ctx): + env = unittest.begin(ctx) + + result = export_for_test.join_flags_list("ws", [ + "-Wl,-rpath,\\$ORIGIN/lib", + "-Wl,-rpath,\\$EXEC_ORIGIN/bin", + ]) + + asserts.equals( + env, + "-Wl,-rpath,\\$ORIGIN/lib -Wl,-rpath,\\$EXEC_ORIGIN/bin", + result, + ) + + return unittest.end(env) + list_to_str_repr_test = unittest.make(_list_to_str_repr_test) +join_flags_list_escapes_loader_tokens_for_shell_test = unittest.make(_join_flags_list_escapes_loader_tokens_for_shell_test) +join_flags_list_preserves_escaped_loader_tokens_for_shell_test = unittest.make(_join_flags_list_preserves_escaped_loader_tokens_for_shell_test) def meson_script_test_suite(): unittest.suite( "meson_script_test_suite", partial.make(list_to_str_repr_test, size = "small"), + partial.make(join_flags_list_escapes_loader_tokens_for_shell_test, size = "small"), + partial.make(join_flags_list_preserves_escaped_loader_tokens_for_shell_test, size = "small"), ) diff --git a/test/runtime_executable_shell_quoting_test.sh b/test/runtime_executable_shell_quoting_test.sh new file mode 100755 index 000000000..14ea22cc9 --- /dev/null +++ b/test/runtime_executable_shell_quoting_test.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# shellcheck disable=SC1090 + +set -euo pipefail + +set +u +f=bazel_tools/tools/bash/runfiles/runfiles.bash +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -d ' ' -f 2-)" 2>/dev/null || \ + source "$0.runfiles/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -d ' ' -f 2-)" 2>/dev/null || { + echo >&2 "cannot find $f" + exit 1 + } +set -u + +if [[ "$#" -ne 1 ]]; then + echo "usage: $0 " >&2 + exit 1 +fi + +wrapper="$(rlocation "$1")" +if [[ -z "$wrapper" || ! -x "$wrapper" ]]; then + echo "runtime executable wrapper is not executable: $1" >&2 + exit 1 +fi + +output="$(USER=expanded "$wrapper")" +if [[ "$output" != "special chars" ]]; then + echo "unexpected runtime executable output: $output" >&2 + exit 1 +fi diff --git a/test/runtime_executable_test.bzl b/test/runtime_executable_test.bzl new file mode 100644 index 000000000..549f94573 --- /dev/null +++ b/test/runtime_executable_test.bzl @@ -0,0 +1,247 @@ +"""Tests for the runtime_executable adapter.""" + +load("@bazel_skylib//lib:partial.bzl", "partial") +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts", "unittest") +load("@rules_cc//cc:defs.bzl", "CcInfo") +load("//foreign_cc:defs.bzl", "runtime_executable") +load("//foreign_cc:providers.bzl", "ForeignCcDepsInfo", "ForeignCcRuntimeExecutableInfo") + +# buildifier: disable=bzl-visibility +load("//foreign_cc/private:transitions.bzl", "extra_toolchains_transitioned_foreign_cc_target") + +def _fake_foreign_cc_impl(ctx): + selected = ctx.actions.declare_file(ctx.label.name + "/bin/tool") + special_chars = ctx.actions.declare_file(ctx.label.name + "/bin/tool-$USER") + other = ctx.actions.declare_file(ctx.label.name + "/debug/tool") + shared_library = ctx.actions.declare_file(ctx.label.name + "/lib/libtool.so") + include_dir = ctx.actions.declare_directory(ctx.label.name + "/include") + static_library = ctx.actions.declare_file(ctx.label.name + "/lib/libtool.a") + interface_library = ctx.actions.declare_file(ctx.label.name + "/lib/tool.ifso") + resource = ctx.actions.declare_file(ctx.label.name + "/share/runtime_resource.txt") + + ctx.actions.write(selected, "#!/usr/bin/env bash\necho selected\n", is_executable = True) + ctx.actions.write(special_chars, "#!/usr/bin/env bash\necho special chars\n", is_executable = True) + ctx.actions.write(other, "#!/usr/bin/env bash\necho other\n", is_executable = True) + ctx.actions.write(shared_library, "shared library\n") + ctx.actions.run_shell( + outputs = [include_dir], + command = "mkdir -p \"$1\" && printf '#define TOOL_H\\n' > \"$1/tool.h\"", + arguments = [include_dir.path], + ) + ctx.actions.write(static_library, "static library\n") + ctx.actions.write(interface_library, "interface library\n") + ctx.actions.write(resource, "expected runtime resource\n") + + outputs = [ + selected, + special_chars, + other, + shared_library, + include_dir, + static_library, + interface_library, + resource, + ] + return [ + DefaultInfo( + files = depset(outputs), + runfiles = ctx.runfiles(), + ), + CcInfo(), + ForeignCcDepsInfo(artifacts = depset()), + ForeignCcRuntimeExecutableInfo( + binaries = { + "debug/tool": other, + "tools/tool": selected, + "tools/tool-$USER": special_chars, + }, + runtime_files = depset( + direct = [ + include_dir, + shared_library, + resource, + ], + transitive = [ + dep[ForeignCcRuntimeExecutableInfo].runtime_files + for dep in ctx.attr.deps + if ForeignCcRuntimeExecutableInfo in dep + ], + ), + ), + OutputGroupInfo(), + ] + +_fake_foreign_cc = rule( + implementation = _fake_foreign_cc_impl, + attrs = { + "deps": attr.label_list(), + }, +) + +def _assert_runtime_executable_contract(env, target_name): + target = analysistest.target_under_test(env) + default_info = target[DefaultInfo] + + files = default_info.files.to_list() + asserts.true( + env, + target_name in [file.basename for file in files], + "adapter executable should be in DefaultInfo.files", + ) + asserts.true( + env, + default_info.files_to_run.executable.basename in [target_name, target_name + ".exe"], + "adapter files_to_run executable should be the public target launcher", + ) + + runfile_short_paths = [ + file.short_path + for file in default_info.default_runfiles.files.to_list() + ] + asserts.true( + env, + "test/runtime_executable_fake_with_dep/bin/tool" in runfile_short_paths, + "selected binary should be in runfiles", + ) + asserts.true( + env, + "test/runtime_executable_fake_with_dep/share/runtime_resource.txt" in runfile_short_paths, + "declared output resources should be in runfiles", + ) + asserts.true( + env, + "test/runtime_executable_fake_with_dep/lib/libtool.so" in runfile_short_paths, + "shared libraries should be in runfiles", + ) + asserts.true( + env, + "test/runtime_executable_fake_with_dep/include" in runfile_short_paths, + "include directory should be in runfiles", + ) + asserts.true( + env, + "test/runtime_executable_fake_dep/share/runtime_resource.txt" in runfile_short_paths, + "transitive declared output resources should be in runfiles", + ) + asserts.false( + env, + "test/runtime_executable_fake_with_dep/bin/tool-$USER" in runfile_short_paths, + "unselected binaries should not be in runtime executable runfiles", + ) + asserts.false( + env, + "test/runtime_executable_fake_with_dep/debug/tool" in runfile_short_paths, + "unselected binaries should not be in runtime executable runfiles", + ) + asserts.false( + env, + "test/runtime_executable_fake_with_dep/lib/libtool.a" in runfile_short_paths, + "static libraries should not be in runtime executable runfiles", + ) + asserts.false( + env, + "test/runtime_executable_fake_with_dep/lib/tool.ifso" in runfile_short_paths, + "interface libraries should not be in runtime executable runfiles", + ) + +# Verifies wrapper mode selects the requested binary, exposes the adapter as the executable, +# and carries the selected binary plus foreign_cc runtime files in runfiles. +def _runtime_executable_exact_binary_test(ctx): + env = analysistest.begin(ctx) + _assert_runtime_executable_contract(env, "runtime_executable_exact_subject") + return analysistest.end(env) + +# Verifies runtime_executable fails during analysis when binary does not exactly match +# an entry from the foreign_cc target's declared out_binaries. +def _runtime_executable_invalid_binary_test(ctx): + env = analysistest.begin(ctx) + asserts.expect_failure( + env, + "runtime_executable binary 'missing' was not found", + ) + return analysistest.end(env) + +# Verifies transitioned foreign_cc wrapper targets preserve the runtime executable metadata. +def _runtime_executable_transitioned_target_test(ctx): + env = analysistest.begin(ctx) + _assert_runtime_executable_contract(env, "runtime_executable_transitioned_subject") + return analysistest.end(env) + +runtime_executable_exact_binary_test = analysistest.make( + _runtime_executable_exact_binary_test, +) + +runtime_executable_invalid_binary_test = analysistest.make( + _runtime_executable_invalid_binary_test, + expect_failure = True, +) + +runtime_executable_transitioned_target_test = analysistest.make( + _runtime_executable_transitioned_target_test, +) + +def runtime_executable_test_suite(name = "runtime_executable_tests"): + """Defines runtime_executable analysis tests. + + Args: + name: Name of the generated test suite. + """ + _fake_foreign_cc(name = "runtime_executable_fake_dep") + + _fake_foreign_cc(name = "runtime_executable_fake") + _fake_foreign_cc( + name = "runtime_executable_fake_with_dep", + deps = [":runtime_executable_fake_dep"], + ) + + extra_toolchains_transitioned_foreign_cc_target( + name = "runtime_executable_transitioned_fake", + target = ":runtime_executable_fake_with_dep", + ) + + runtime_executable( + name = "runtime_executable_exact_subject", + binary = "tools/tool", + foreign_cc_target = ":runtime_executable_fake_with_dep", + tags = ["manual"], + ) + + runtime_executable( + name = "runtime_executable_invalid_subject", + binary = "missing", + foreign_cc_target = ":runtime_executable_fake_with_dep", + tags = ["manual"], + ) + + runtime_executable( + name = "runtime_executable_transitioned_subject", + binary = "tools/tool", + foreign_cc_target = ":runtime_executable_transitioned_fake", + tags = ["manual"], + ) + + runtime_executable( + name = "runtime_executable_shell_quoting_subject", + binary = "tools/tool-$USER", + foreign_cc_target = ":runtime_executable_fake_with_dep", + tags = ["manual"], + ) + + unittest.suite( + name, + partial.make( + runtime_executable_exact_binary_test, + size = "small", + target_under_test = ":runtime_executable_exact_subject", + ), + partial.make( + runtime_executable_invalid_binary_test, + size = "small", + target_under_test = ":runtime_executable_invalid_subject", + ), + partial.make( + runtime_executable_transitioned_target_test, + size = "small", + target_under_test = ":runtime_executable_transitioned_subject", + ), + ) diff --git a/test/runtime_search/BUILD.bazel b/test/runtime_search/BUILD.bazel new file mode 100644 index 000000000..8d501ec26 --- /dev/null +++ b/test/runtime_search/BUILD.bazel @@ -0,0 +1,74 @@ +load("//foreign_cc:defs.bzl", "configure_make", "make", "meson") + +# This files contains targets used by runtime search tests. + +package(default_visibility = ["//test:__pkg__"]) + +filegroup( + name = "contract_src", + testonly = True, + + # These analysis-failure fixtures never build their source inputs. The + # filegroup only gives foreign_cc rules a stable lib_source label. + srcs = ["BUILD.bazel"], +) + +make( + name = "make_missing_shared_ldflags_vars_explicit", + testonly = True, + lib_source = ":contract_src", + out_include_dir = "", + out_shared_libs = ["libruntime_search_contract.so"], + runtime_library_search_directories = "enabled", + tags = ["manual"], +) + +make( + name = "make_missing_shared_ldflags_vars_auto", + testonly = True, + lib_source = ":contract_src", + out_include_dir = "", + out_shared_libs = ["libruntime_search_contract.so"], + runtime_library_search_directories = "auto", + tags = ["manual"], +) + +configure_make( + name = "configure_make_missing_shared_ldflags_vars_explicit", + testonly = True, + lib_source = ":contract_src", + out_include_dir = "", + out_shared_libs = ["libruntime_search_contract.so"], + runtime_library_search_directories = "enabled", + tags = ["manual"], +) + +configure_make( + name = "configure_make_missing_shared_ldflags_vars_auto", + testonly = True, + lib_source = ":contract_src", + out_include_dir = "", + out_shared_libs = ["libruntime_search_contract.so"], + runtime_library_search_directories = "auto", + tags = ["manual"], +) + +meson( + name = "meson_missing_shared_ldflags_option_explicit", + testonly = True, + lib_source = ":contract_src", + out_include_dir = "", + out_shared_libs = ["libruntime_search_contract.so"], + runtime_library_search_directories = "enabled", + tags = ["manual"], +) + +meson( + name = "meson_missing_shared_ldflags_option_auto", + testonly = True, + lib_source = ":contract_src", + out_include_dir = "", + out_shared_libs = ["libruntime_search_contract.so"], + runtime_library_search_directories = "auto", + tags = ["manual"], +) diff --git a/test/runtime_search_paths_test.bzl b/test/runtime_search_paths_test.bzl new file mode 100644 index 000000000..9751282a4 --- /dev/null +++ b/test/runtime_search_paths_test.bzl @@ -0,0 +1,483 @@ +"""Unit tests for runtime library search directory path derivation. + +This suite covers pure helper behavior: self-output origins, custom output +directories, install-tree-relative additional origins, data-output install-root +anchors, deduping, solib sibling paths, and external-repo normalization. It +does not instantiate foreign_cc rules or validate analysis-time policy. +""" + +load("@bazel_skylib//lib:partial.bzl", "partial") +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") + +# buildifier: disable=bzl-visibility +load( + "//foreign_cc/private:runtime_library_search_directories.bzl", + "runtime_library_search_directories", +) + +# buildifier: disable=bzl-visibility +load( + "//foreign_cc/private:runtime_search_paths.bzl", + "export_for_test", +) + +def _ctx( + attr = struct(), + package = "pkg"): + return struct( + attr = struct( + additional_dynamic_runtime_library_search_origins = getattr(attr, "additional_dynamic_runtime_library_search_origins", []), + additional_executable_runtime_library_search_origins = getattr(attr, "additional_executable_runtime_library_search_origins", []), + out_bin_dir = getattr(attr, "out_bin_dir", "bin"), + out_binaries = getattr(attr, "out_binaries", []), + out_data_dirs = getattr(attr, "out_data_dirs", []), + out_data_files = getattr(attr, "out_data_files", []), + out_lib_dir = getattr(attr, "out_lib_dir", "lib"), + out_shared_libs = getattr(attr, "out_shared_libs", []), + runtime_library_search_directories = getattr(attr, "runtime_library_search_directories", "disabled"), + ), + label = struct( + package = package, + ), + ) + +def _file(short_path): + return struct(short_path = short_path) + +def _outputs( + shared_files = [], + binary_files = [], + data_dirs = [], + data_files = []): + return struct( + libraries = struct( + shared_libraries = [_file(short_path) for short_path in shared_files], + ), + out_binary_files = [_file(short_path) for short_path in binary_files], + data_dirs = [_file(short_path) for short_path in data_dirs], + data_files = [_file(short_path) for short_path in data_files], + ) + +def _runtime_library_search_directories( + ctx, + shared_files = [], + binary_files = [], + data_dirs = [], + data_files = []): + return runtime_library_search_directories( + ctx, + _outputs( + shared_files = shared_files, + binary_files = binary_files, + data_dirs = data_dirs, + data_files = data_files, + ), + ) + +def _to_list_or_none(value): + if value == None: + return None + return value.to_list() + +def _assert_runtime_library_search_directories( + env, + case): + outputs = getattr(case, "outputs", struct()) + + result = _runtime_library_search_directories( + case.ctx, + shared_files = getattr(outputs, "shared_files", []), + binary_files = getattr(outputs, "binary_files", []), + data_dirs = getattr(outputs, "data_dirs", []), + data_files = getattr(outputs, "data_files", []), + ) + + asserts.equals(env, case.expected.shared_dirs, _to_list_or_none(result.shared_dirs), "{} shared".format(case.name)) + asserts.equals(env, case.expected.executable_dirs, _to_list_or_none(result.executable_dirs), "{} executable".format(case.name)) + +def _self_output_origins_are_derived_from_outputs_test(ctx): + env = unittest.begin(ctx) + + cases = [ + struct( + name = "shared output", + ctx = _ctx(attr = struct( + runtime_library_search_directories = "enabled", + out_shared_libs = ["libpython3.10.so"], + )), + outputs = struct( + shared_files = ["pkg/python/lib/libpython3.10.so"], + ), + expected = struct( + shared_dirs = ["."], + executable_dirs = [], + ), + ), + struct( + name = "data dir output", + ctx = _ctx(attr = struct( + runtime_library_search_directories = "enabled", + out_data_dirs = ["lib/python3.10/lib-dynload"], + )), + outputs = struct( + data_dirs = ["pkg/python/lib/python3.10/lib-dynload"], + ), + expected = struct( + shared_dirs = [], + executable_dirs = [], + ), + ), + struct( + name = "binary and shared outputs", + ctx = _ctx(attr = struct( + runtime_library_search_directories = "enabled", + out_binaries = ["python3.10"], + out_shared_libs = ["libpython3.10.so"], + )), + outputs = struct( + binary_files = ["pkg/python/bin/python3.10"], + shared_files = ["pkg/python/lib/libpython3.10.so"], + ), + expected = struct( + shared_dirs = ["."], + executable_dirs = ["../lib"], + ), + ), + struct( + name = "shared and data outputs", + ctx = _ctx(attr = struct( + runtime_library_search_directories = "enabled", + out_data_dirs = ["bin"], + out_shared_libs = ["libpython3.10.so"], + )), + outputs = struct( + data_dirs = ["pkg/python/bin"], + shared_files = ["pkg/python/lib/libpython3.10.so"], + ), + expected = struct( + shared_dirs = ["."], + executable_dirs = [], + ), + ), + ] + + for case in cases: + _assert_runtime_library_search_directories(env, case) + + return unittest.end(env) + +def _custom_output_dirs_derive_self_output_origins_test(ctx): + env = unittest.begin(ctx) + + case = struct( + name = "custom output directories", + ctx = _ctx(attr = struct( + runtime_library_search_directories = "enabled", + out_bin_dir = "tools/bin", + out_binaries = ["python3.10"], + out_lib_dir = "lib64", + out_shared_libs = ["libpython3.10.so"], + )), + outputs = struct( + binary_files = ["pkg/python/tools/bin/python3.10"], + shared_files = ["pkg/python/lib64/libpython3.10.so"], + ), + expected = struct( + shared_dirs = ["."], + executable_dirs = ["../../lib64"], + ), + ) + + _assert_runtime_library_search_directories(env, case) + + return unittest.end(env) + +def _additional_origins_are_install_tree_relative_test(ctx): + env = unittest.begin(ctx) + + cases = [ + struct( + name = "additional dynamic origin does not affect executable origins", + ctx = _ctx(attr = struct( + additional_dynamic_runtime_library_search_origins = ["lib/python3.10/lib-dynload"], + runtime_library_search_directories = "enabled", + out_binaries = ["python3.10"], + out_shared_libs = ["libpython3.10.so"], + )), + outputs = struct( + binary_files = ["pkg/python/bin/python3.10"], + shared_files = ["pkg/python/lib/libpython3.10.so"], + ), + expected = struct( + shared_dirs = [".", "../.."], + executable_dirs = ["../lib"], + ), + ), + struct( + name = "additional executable origin does not affect dynamic origins", + ctx = _ctx(attr = struct( + additional_executable_runtime_library_search_origins = ["libexec"], + runtime_library_search_directories = "enabled", + out_shared_libs = ["libpython3.10.so"], + )), + outputs = struct( + shared_files = ["pkg/python/lib/libpython3.10.so"], + ), + expected = struct( + shared_dirs = ["."], + executable_dirs = ["../lib"], + ), + ), + struct( + name = "self binary output and additional executable origin", + ctx = _ctx(attr = struct( + additional_executable_runtime_library_search_origins = ["libexec/bin"], + runtime_library_search_directories = "enabled", + out_binaries = ["python3.10"], + out_shared_libs = ["libpython3.10.so"], + )), + outputs = struct( + binary_files = ["pkg/python/bin/python3.10"], + shared_files = ["pkg/python/lib/libpython3.10.so"], + ), + expected = struct( + shared_dirs = ["."], + executable_dirs = ["../lib", "../../lib"], + ), + ), + struct( + name = "self binary output and additional executable origin but no shared output", + ctx = _ctx(attr = struct( + additional_executable_runtime_library_search_origins = ["libexec/bin"], + runtime_library_search_directories = "enabled", + out_binaries = ["python3.10"], + )), + outputs = struct( + binary_files = ["pkg/python/bin/python3.10"], + ), + expected = struct( + shared_dirs = [], + executable_dirs = [], + ), + ), + struct( + name = "data dir with additional dynamic origin", + ctx = _ctx(attr = struct( + additional_dynamic_runtime_library_search_origins = ["lib/python3.10/lib-dynload"], + runtime_library_search_directories = "enabled", + out_data_dirs = ["lib/python3.10/lib-dynload"], + )), + outputs = struct( + data_dirs = ["pkg/python/lib/python3.10/lib-dynload"], + ), + expected = struct( + shared_dirs = [], + executable_dirs = [], + ), + ), + struct( + name = "data file with additional dynamic origin", + ctx = _ctx(attr = struct( + additional_dynamic_runtime_library_search_origins = ["lib/python3.10/lib-dynload"], + runtime_library_search_directories = "enabled", + out_data_files = ["lib/python3.10/lib-dynload/ext.so"], + )), + outputs = struct( + data_files = ["pkg/python/lib/python3.10/lib-dynload/ext.so"], + ), + expected = struct( + shared_dirs = [], + executable_dirs = [], + ), + ), + ] + + for case in cases: + _assert_runtime_library_search_directories(env, case) + + return unittest.end(env) + +def _runtime_search_directories_dedupe_self_library_dirs_test(ctx): + env = unittest.begin(ctx) + + case = struct( + name = "duplicate self library dirs", + ctx = _ctx(attr = struct( + runtime_library_search_directories = "enabled", + out_shared_libs = [ + "libpython3.10.so", + "libother.so", + ], + )), + outputs = struct( + shared_files = [ + "pkg/python/lib/libpython3.10.so", + "pkg/python/lib/libother.so", + ], + ), + expected = struct( + shared_dirs = ["."], + executable_dirs = [], + ), + ) + + _assert_runtime_library_search_directories(env, case) + + return unittest.end(env) + +def _solib_sibling_search_directories_test(ctx): + env = unittest.begin(ctx) + + cases = [ + struct( + name = "solib output and sibling rpaths", + origins = ["thirdparty/python39/python39/lib/python3.9/lib-dynload"], + dep_library_dirs = ["_solib_local/_Uthirdparty_Sbzip2"], + expected = [ + "../../../../../../_solib_local/_Uthirdparty_Sbzip2", + "../_Uthirdparty_Sbzip2", + ], + ), + struct( + name = "non-solib output rpath only", + origins = ["pkg/python/bin"], + dep_library_dirs = ["pkg/python/lib"], + expected = ["../lib"], + ), + struct( + name = "solib without directory", + origins = ["pkg/python/lib"], + dep_library_dirs = ["_solib_local"], + expected = ["../../../_solib_local"], + ), + ] + + for case in cases: + result = export_for_test.search_directories( + case.origins, + case.dep_library_dirs, + ) + export_for_test.solib_sibling_search_directories(case.dep_library_dirs) + + asserts.equals(env, case.expected, result, case.name) + + return unittest.end(env) + +def _external_repo_origins_use_execroot_relative_rpaths_test(ctx): + env = unittest.begin(ctx) + + cases = [ + struct( + name = "main repo output to external repo dependency", + origins = ["myproj/build/lib"], + library_dirs = ["../zlib/lib"], + expected = ["../../../external/zlib/lib"], + ), + struct( + name = "external repo output to external repo dependency", + origins = ["../consumer/pkg/lib"], + library_dirs = ["../zlib/lib"], + expected = ["../../../zlib/lib"], + ), + ] + + for case in cases: + result = export_for_test.search_directories( + case.origins, + case.library_dirs, + ) + + asserts.equals(env, case.expected, result, case.name) + + return unittest.end(env) + +def _outputs_anchor_installdir_test(ctx): + env = unittest.begin(ctx) + + cases = [ + struct( + name = "shared library anchor for installdir", + ctx = _ctx(attr = struct( + out_shared_libs = ["libpython3.10.so"], + )), + outputs = _outputs( + shared_files = ["pkg/python/lib/libpython3.10.so"], + ), + expected = "pkg/python", + ), + struct( + name = "binary anchor for installdir", + ctx = _ctx(attr = struct( + out_binaries = ["python3.10"], + )), + outputs = _outputs( + binary_files = ["pkg/python/bin/python3.10"], + ), + expected = "pkg/python", + ), + struct( + name = "data dir anchor for installdir", + ctx = _ctx(attr = struct( + out_data_dirs = ["lib/python3.10/lib-dynload"], + )), + outputs = _outputs( + data_dirs = ["pkg/python/lib/python3.10/lib-dynload"], + ), + expected = "pkg/python", + ), + struct( + name = "data file anchor for installdir", + ctx = _ctx(attr = struct( + out_data_files = ["lib/python3.10/lib-dynload/ext.so"], + )), + outputs = _outputs( + data_files = ["pkg/python/lib/python3.10/lib-dynload/ext.so"], + ), + expected = "pkg/python", + ), + ] + + for case in cases: + anchor = export_for_test.installdir_from_outputs(case.ctx, case.outputs) + asserts.equals(env, case.expected, anchor, case.name) + + return unittest.end(env) + +self_output_origins_are_derived_from_outputs_test = unittest.make( + _self_output_origins_are_derived_from_outputs_test, +) +custom_output_dirs_derive_self_output_origins_test = unittest.make( + _custom_output_dirs_derive_self_output_origins_test, +) +additional_origins_are_install_tree_relative_test = unittest.make( + _additional_origins_are_install_tree_relative_test, +) +outputs_anchor_installdir_test = unittest.make( + _outputs_anchor_installdir_test, +) +runtime_search_directories_dedupe_self_library_dirs_test = unittest.make( + _runtime_search_directories_dedupe_self_library_dirs_test, +) +solib_sibling_search_directories_test = unittest.make( + _solib_sibling_search_directories_test, +) +external_repo_origins_use_execroot_relative_rpaths_test = unittest.make( + _external_repo_origins_use_execroot_relative_rpaths_test, +) + +def runtime_library_search_directories_test_suite_paths(name = "runtime_library_search_directories_test_suite_paths"): + """Declares runtime-library-search helper path tests. + + Args: + name: Name for the generated unittest suite. + """ + + unittest.suite( + name, + partial.make(self_output_origins_are_derived_from_outputs_test, size = "small"), + partial.make(custom_output_dirs_derive_self_output_origins_test, size = "small"), + partial.make(additional_origins_are_install_tree_relative_test, size = "small"), + partial.make(outputs_anchor_installdir_test, size = "small"), + partial.make(runtime_search_directories_dedupe_self_library_dirs_test, size = "small"), + partial.make(solib_sibling_search_directories_test, size = "small"), + partial.make(external_repo_origins_use_execroot_relative_rpaths_test, size = "small"), + ) diff --git a/test/runtime_search_policy_test.bzl b/test/runtime_search_policy_test.bzl new file mode 100644 index 000000000..e62e79afa --- /dev/null +++ b/test/runtime_search_policy_test.bzl @@ -0,0 +1,291 @@ +"""Unit tests for runtime library search directory policy. + +This suite covers enablement and analysis-time contracts: global auto +enablement, disabled overrides, the Windows explicit-vs-auto split, and +foreign_cc rule failures when shared-library outputs are declared without the +required shared-link flag hook. +""" + +load("@bazel_skylib//lib:partial.bzl", "partial") +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts", "unittest") + +# buildifier: disable=bzl-visibility +load( + "//foreign_cc/private:runtime_library_search_directories.bzl", + "RUNTIME_LIBRARY_SEARCH_DIRECTORY_ATTRIBUTES", + "export_for_test", + "runtime_library_search_directories_enabled", +) + +_RuntimeSearchEnabledInfo = provider( + "Runtime library search directory enablement state.", + fields = [ + "requested_enabled", + "enabled", + ], +) +_RUNTIME_SEARCH_SETTING = str(Label("//foreign_cc/settings:runtime_library_search_directories")) +_UNIX_ONLY = select({ + "@platforms//os:windows": ["@platforms//:incompatible"], + "//conditions:default": [], +}) + +def _missing_shared_ldflags_messages(shared_ldflags_hook): + return [ + "{} is not set".format(shared_ldflags_hook), + "out_shared_libs", + "runtime_library_search_directories = \"disabled\"", + ] + +def _runtime_search_enabled_subject_impl(ctx): + return [_RuntimeSearchEnabledInfo( + requested_enabled = export_for_test.runtime_library_search_directories_requested(ctx), + enabled = runtime_library_search_directories_enabled( + ctx, + is_windows = ctx.attr.is_windows, + ), + )] + +_runtime_search_enabled_subject = rule( + implementation = _runtime_search_enabled_subject_impl, + attrs = { + "is_windows": attr.bool(default = False), + } | dict(RUNTIME_LIBRARY_SEARCH_DIRECTORY_ATTRIBUTES), +) + +# Generic enablement test: asserts the (requested, enabled) pair the subject +# resolves to. Inputs (the subject target and global setting) and expectations +# (expected_requested / expected_enabled) both live at the partial.make call +# site, so each case reads as input -> expected in the suite below. +def _enablement_test_impl(ctx): + env = analysistest.begin(ctx) + + info = analysistest.target_under_test(env)[_RuntimeSearchEnabledInfo] + asserts.equals(env, ctx.attr.expected_requested, info.requested_enabled, "requested_enabled") + asserts.equals(env, ctx.attr.expected_enabled, info.enabled, "enabled") + + return analysistest.end(env) + +# Generic failure test: asserts every expected_messages substring appears in the +# analysis failure of the target under test. +def _expect_failure_messages_test_impl(ctx): + env = analysistest.begin(ctx) + + for message in ctx.attr.expected_messages: + asserts.expect_failure(env, message) + + return analysistest.end(env) + +# The global build setting is baked into the rule via config_settings, so each +# global value needs its own rule object. All reuse the one impl. +enablement_test = analysistest.make( + _enablement_test_impl, + config_settings = { + _RUNTIME_SEARCH_SETTING: "enabled", + }, + attrs = { + "expected_enabled": attr.bool(mandatory = True), + "expected_requested": attr.bool(mandatory = True), + }, +) +enablement_disabled_global_test = analysistest.make( + _enablement_test_impl, + # No config_settings: the global setting stays at its "disabled" default. + attrs = { + "expected_enabled": attr.bool(mandatory = True), + "expected_requested": attr.bool(mandatory = True), + }, +) +expect_failure_messages_test = analysistest.make( + _expect_failure_messages_test_impl, + expect_failure = True, + attrs = { + "expected_messages": attr.string_list(mandatory = True), + }, +) +expect_failure_messages_auto_test = analysistest.make( + _expect_failure_messages_test_impl, + config_settings = { + _RUNTIME_SEARCH_SETTING: "enabled", + }, + expect_failure = True, + attrs = { + "expected_messages": attr.string_list(mandatory = True), + }, +) + +def runtime_library_search_directories_test_suite_policy(name = "runtime_library_search_directories_test_suite_policy"): + """Declares runtime-library-search enablement and analysis-policy tests. + + Args: + name: Name for the generated analysistest suite and helper targets. + """ + + _runtime_search_enabled_subject( + name = name + "_auto_subject", + runtime_library_search_directories = "auto", + tags = ["manual"], + ) + _runtime_search_enabled_subject( + name = name + "_disabled_subject", + runtime_library_search_directories = "disabled", + tags = ["manual"], + ) + _runtime_search_enabled_subject( + name = name + "_enabled_subject", + runtime_library_search_directories = "enabled", + tags = ["manual"], + ) + _runtime_search_enabled_subject( + name = name + "_windows_explicit_subject", + is_windows = True, + runtime_library_search_directories = "enabled", + tags = ["manual"], + ) + _runtime_search_enabled_subject( + name = name + "_windows_auto_subject", + is_windows = True, + runtime_library_search_directories = "auto", + tags = ["manual"], + ) + + unittest.suite( + name, + # Enablement matrix. The subject's runtime_library_search_directories + # attr and the global setting together decide (requested, enabled); + # "auto" is the only attr value that reads the global setting, and + # is_windows forces enabled off (explicit "enabled" fails instead; see + # below). The global setting's own default is "disabled". + # + # subject attr | is_windows | global setting | requested | enabled + # -------------+------------+----------------+-----------+-------- + # auto | no | enabled | True | True + # disabled | no | enabled | False | False + # auto | yes | enabled | True | False + # auto | no | disabled | False | False + # enabled | no | disabled | True | True + # + # Rows 1-3: global setting forced to "enabled" via config_settings. + partial.make( + enablement_test, + name = name + "_auto_uses_global_test", + size = "small", + target_under_test = ":" + name + "_auto_subject", + expected_requested = True, + expected_enabled = True, + ), + partial.make( + enablement_test, + name = name + "_disabled_overrides_global_test", + size = "small", + target_under_test = ":" + name + "_disabled_subject", + expected_requested = False, + expected_enabled = False, + ), + partial.make( + enablement_test, + name = name + "_windows_auto_noops_test", + size = "small", + target_under_test = ":" + name + "_windows_auto_subject", + expected_requested = True, + expected_enabled = False, + ), + # Rows 4-5 above: global setting left at its "disabled" default. + partial.make( + enablement_disabled_global_test, + name = name + "_auto_follows_global_disabled_test", + size = "small", + target_under_test = ":" + name + "_auto_subject", + expected_requested = False, + expected_enabled = False, + ), + partial.make( + enablement_disabled_global_test, + name = name + "_explicit_enabled_ignores_global_test", + size = "small", + target_under_test = ":" + name + "_enabled_subject", + expected_requested = True, + expected_enabled = True, + ), + # Windows explicit "enabled" is a hard analysis failure. + partial.make( + expect_failure_messages_test, + name = name + "_windows_explicit_fails_test", + size = "small", + target_under_test = ":" + name + "_windows_explicit_subject", + expected_messages = [ + "runtime_library_search_directories = \"enabled\"", + "not supported on Windows", + name + "_windows_explicit_subject", + ], + ), + + # Rules that declare out_shared_libs must wire up the rule-specific + # shared-link flag hook whenever runtime search is enabled; missing it is + # a hard analysis failure. The cases below cover each (rule kind x hook + # attr x enablement path) combination: + # + # rule kind | hook attr | enablement | test rule + # ----------------+----------------------+--------------+---------------------------------- + # make | shared_ldflags_vars | explicit | expect_failure_messages_test + # make | shared_ldflags_vars | auto+global | expect_failure_messages_auto_test + # configure_make | shared_ldflags_vars | explicit | expect_failure_messages_test + # configure_make | shared_ldflags_vars | auto+global | expect_failure_messages_auto_test + # meson | shared_ldflags_option| explicit | expect_failure_messages_test + # meson | shared_ldflags_option| auto+global | expect_failure_messages_auto_test + # + # "explicit": sets runtime_library_search_directories = "enabled" on + # the target. + # "auto+global": sets runtime_library_search_directories = "auto" on the + # the target and forces the build_setting for the same + # feature to be "enabled" via config_settings. + # All are non-Windows only (_UNIX_ONLY). + partial.make( + expect_failure_messages_test, + name = "runtime_search_make_missing_shared_ldflags_vars_explicit_test", + size = "small", + target_under_test = "//test/runtime_search:make_missing_shared_ldflags_vars_explicit", + expected_messages = _missing_shared_ldflags_messages("shared_ldflags_vars"), + target_compatible_with = _UNIX_ONLY, + ), + partial.make( + expect_failure_messages_auto_test, + name = "runtime_search_make_missing_shared_ldflags_vars_auto_test", + size = "small", + target_under_test = "//test/runtime_search:make_missing_shared_ldflags_vars_auto", + expected_messages = _missing_shared_ldflags_messages("shared_ldflags_vars"), + target_compatible_with = _UNIX_ONLY, + ), + partial.make( + expect_failure_messages_test, + name = "runtime_search_configure_make_missing_shared_ldflags_vars_explicit_test", + size = "small", + target_under_test = "//test/runtime_search:configure_make_missing_shared_ldflags_vars_explicit", + expected_messages = _missing_shared_ldflags_messages("shared_ldflags_vars"), + target_compatible_with = _UNIX_ONLY, + ), + partial.make( + expect_failure_messages_auto_test, + name = "runtime_search_configure_make_missing_shared_ldflags_vars_auto_test", + size = "small", + target_under_test = "//test/runtime_search:configure_make_missing_shared_ldflags_vars_auto", + expected_messages = _missing_shared_ldflags_messages("shared_ldflags_vars"), + target_compatible_with = _UNIX_ONLY, + ), + partial.make( + expect_failure_messages_test, + name = "runtime_search_meson_missing_shared_ldflags_option_explicit_test", + size = "small", + target_under_test = "//test/runtime_search:meson_missing_shared_ldflags_option_explicit", + expected_messages = _missing_shared_ldflags_messages("shared_ldflags_option"), + target_compatible_with = _UNIX_ONLY, + ), + partial.make( + expect_failure_messages_auto_test, + name = "runtime_search_meson_missing_shared_ldflags_option_auto_test", + size = "small", + target_under_test = "//test/runtime_search:meson_missing_shared_ldflags_option_auto", + expected_messages = _missing_shared_ldflags_messages("shared_ldflags_option"), + target_compatible_with = _UNIX_ONLY, + ), + ) diff --git a/test/runtime_search_test.bzl b/test/runtime_search_test.bzl new file mode 100644 index 000000000..8a74fef11 --- /dev/null +++ b/test/runtime_search_test.bzl @@ -0,0 +1,31 @@ +"""Aggregates runtime library search directory tests.""" + +load( + ":runtime_search_paths_test.bzl", + "runtime_library_search_directories_test_suite_paths", +) +load( + ":runtime_search_policy_test.bzl", + "runtime_library_search_directories_test_suite_policy", +) + +def runtime_library_search_directories_test_suite(name = "runtime_library_search_directories_test_suite"): + """Declares runtime-library-search helper and analysis-policy tests. + + Args: + name: Name for the public aggregate test suite. + """ + + paths_suite = name + "_paths" + policy_suite = name + "_policy" + + runtime_library_search_directories_test_suite_paths(name = paths_suite) + runtime_library_search_directories_test_suite_policy(name = policy_suite) + + native.test_suite( + name = name, + tests = [ + paths_suite, + policy_suite, + ], + )