pybind_library_test: fix PYTHONHOME setup build_defs.bzl: * Added a new pybind_py_env_test rule. This rule resolves the Python toolchain at analysis time, extracts the runtime path, and generates a shell wrapper script. * The wrapper script dynamically identifies the absolute path to the Python runtime within the Bazel runfiles and sets the PYTHONHOME environment variable before executing the actual C++ test binary. * Updated the pybind_library_test macro to build the C++ code as a private cc_binary and then wrap it with pybind_py_env_test.
diff --git a/build_defs.bzl b/build_defs.bzl index fca4bca..6b0fd2b 100644 --- a/build_defs.bzl +++ b/build_defs.bzl
@@ -10,6 +10,80 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library") load("@rules_cc//cc:cc_test.bzl", "cc_test") +def _pybind_py_env_test_impl(ctx): + toolchain = ctx.toolchains["@rules_python//python:toolchain_type"] + py3_runtime = toolchain.py3_runtime + if not py3_runtime: + fail("No python3 runtime found in toolchain") + + # On Windows, we cannot use the shell script wrapper. + if ctx.target_platform_has_constraint(ctx.attr._windows_constraint[platform_common.ConstraintValueInfo]): + # On Windows, we need to return an executable created by this rule. + # We create a symlink to the actual binary. + # We use the same extension as the original binary (usually .exe). + extension = ctx.executable.binary.extension + executable = ctx.actions.declare_file(ctx.label.name + ("." + extension if extension else "")) + ctx.actions.symlink(output = executable, target_file = ctx.executable.binary, is_executable = True) + return [ + DefaultInfo( + executable = executable, + runfiles = ctx.runfiles(files = [executable]) + .merge(ctx.attr.binary[DefaultInfo].default_runfiles) + .merge(ctx.runfiles(transitive_files = py3_runtime.files)), + ), + ] + + interpreter = py3_runtime.interpreter + + # Generate a wrapper script that sets PYTHONHOME and runs the C++ binary. + script = ctx.actions.declare_file(ctx.label.name + ".sh") + + content = "#!/bin/bash\n" + content += "if [ -z \"$RUNFILES_DIR\" ]; then\n" + content += " if [ -d \"$0.runfiles\" ]; then\n" + content += " RUNFILES_DIR=\"$0.runfiles\"\n" + content += " else\n" + content += " RUNFILES_DIR=\"$(dirname \"$0\")/../..\"\n" + content += " fi\n" + content += "fi\n" + content += "INTERPRETER_PATH=\"$RUNFILES_DIR/" + ctx.workspace_name + "/" + interpreter.short_path + "\"\n" + content += "if [ ! -f \"$INTERPRETER_PATH\" ]; then\n" + content += " INTERPRETER_PATH=$(find \"$RUNFILES_DIR\" -path \"*/" + interpreter.short_path + "\" | head -n 1)\n" + content += "fi\n" + content += "export PYTHONHOME=$(dirname $(dirname $(readlink -f \"$INTERPRETER_PATH\")))\n" + content += "BINARY_PATH=\"$RUNFILES_DIR/" + ctx.workspace_name + "/" + ctx.executable.binary.short_path + "\"\n" + content += "if [ ! -f \"$BINARY_PATH\" ]; then\n" + content += " BINARY_PATH=$(find \"$RUNFILES_DIR\" -path \"*/" + ctx.executable.binary.short_path + "\" | head -n 1)\n" + content += "fi\n" + content += "exec \"$BINARY_PATH\" \"$@\"\n" + + ctx.actions.write(script, content, is_executable = True) + + runfiles = ctx.runfiles(files = [script, ctx.executable.binary]) + runfiles = runfiles.merge(ctx.attr.binary[DefaultInfo].default_runfiles) + runfiles = runfiles.merge(ctx.runfiles(transitive_files = py3_runtime.files)) + + return [ + DefaultInfo( + executable = script, + runfiles = runfiles, + ), + ] + +pybind_py_env_test = rule( + implementation = _pybind_py_env_test_impl, + test = True, + attrs = { + "binary": attr.label( + executable = True, + cfg = "target", + mandatory = True, + ), + "_windows_constraint": attr.label(default = "@platforms//os:windows"), + }, + toolchains = ["@rules_python//python:toolchain_type"], +) + def register_extension_info(**kwargs): pass @@ -148,17 +222,35 @@ # Mark common dependencies as required for build_cleaner. tags = tags + ["req_dep=%s" % dep for dep in PYBIND_DEPS] - cc_test( - name = name, + # Pop test-only attributes that cc_binary doesn't support. + test_kwargs = {} + for attr in ["size", "timeout", "flaky", "shard_count", "local"]: + if attr in kwargs: + test_kwargs[attr] = kwargs.pop(attr) + + # Build the actual C++ binary. + cc_binary( + name = name + "_bin", copts = copts + PYBIND_COPTS, features = features + PYBIND_FEATURES, - tags = tags, + testonly = True, + visibility = ["//visibility:private"], deps = deps + PYBIND_DEPS + [ "@rules_python//python/cc:current_py_cc_libs", ], **kwargs ) + # Use a wrapper rule to set PYTHONHOME and run the binary. + pybind_py_env_test( + name = name, + binary = ":" + name + "_bin", + testonly = True, + tags = tags, + visibility = kwargs.get("visibility"), + **test_kwargs + ) + # Register extension with build_cleaner. register_extension_info( extension = pybind_extension,