diff --git a/python/private/cc/py_extension_macro.bzl b/python/private/cc/py_extension_macro.bzl index 8c4cc36f58..3a5aa0d72f 100644 --- a/python/private/cc/py_extension_macro.bzl +++ b/python/private/cc/py_extension_macro.bzl @@ -1,33 +1,96 @@ -"""Wrapper macro for the py_extension rule.""" +"""Macro for creating Python extensions.""" +load("@rules_cc//cc:cc_library.bzl", "cc_library") load("@rules_cc//cc:cc_shared_library.bzl", "cc_shared_library") load("//python/private:util.bzl", "add_tag") -load( - ":py_extension_rule.bzl", - _py_extension = "py_extension", - ##_py_extension_csl_rule = "py_extension_csl", -) +load(":py_extension_rule.bzl", "py_extension_wrapper") -def py_extension(**kwargs): - """A macro that calls the py_extension rule and adds a tag. +def py_extension( + name, + srcs = None, + hdrs = None, + copts = None, + defines = None, + deps = None, + dynamic_deps = None, + exports_filter = None, + user_link_flags = None, + visibility = None, + data = None, + **kwargs): + """Creates a Python extension module. Args: - **kwargs: Additional arguments to pass to the rule. + name: Target name. + srcs: Optional C/C++ source files to compile directly for this extension. + hdrs: Optional header files for the srcs. + copts: Optional compiler flags for srcs. + defines: Optional preprocessor defines for srcs. + deps: cc_library targets to statically link into the extension. + dynamic_deps: cc_shared_library targets to dynamically link. + exports_filter: Filter for exported symbols passed to cc_shared_library. + user_link_flags: Additional link flags passed to cc_shared_library. + visibility: Target visibility. + data: Optional list of files or targets needed by this extension at runtime. + **kwargs: Additional arguments passed to the underlying wrapper rule. """ add_tag(kwargs, "@rules_python//python/cc:py_extension") - use_csl = kwargs.pop("use_csl", False) - if use_csl: - _py_extension_csl(**kwargs) - else: - _py_extension(**kwargs) + csl_deps = [] -def _py_extension_csl(*, name, module_name = None, **kwargs): - if not module_name: - module_name = name + # 1. Handle user-supplied static deps + if deps: + csl_deps.extend(deps) + + # 2. If srcs or hdrs are specified, create an implicit cc_library for them + if srcs or hdrs: + impl_lib_name = "_" + name + "_impl" + cc_library( + name = impl_lib_name, + srcs = srcs, + hdrs = hdrs, + copts = (copts or []) + ["-fPIC"], + defines = defines, + deps = ["@rules_python//python/cc:current_py_cc_headers"], + visibility = ["//visibility:private"], + ) + csl_deps.append(":" + impl_lib_name) + + # 3. If no static deps or sources were specified, use empty target for CSL requirement + if not csl_deps: + csl_deps.append("//python/private/cc:empty") + + # 4. Create the underlying cc_shared_library + csl_name = "_" + name + "_csl" + csl_kwargs = {} + if exports_filter: + csl_kwargs["exports_filter"] = exports_filter + if user_link_flags: + csl_kwargs["user_link_flags"] = user_link_flags cc_shared_library( + name = csl_name, + deps = csl_deps, + dynamic_deps = dynamic_deps, + visibility = ["//visibility:private"], + **csl_kwargs + ) + + # 5. Select default libc constraint if not provided + if "libc" not in kwargs: + kwargs["libc"] = select({ + "@rules_python//python/config_settings:_is_py_linux_libc_glibc": "glibc", + "@rules_python//python/config_settings:_is_py_linux_libc_musl": "musl", + "//conditions:default": "glibc", + }) + + if data != None: + kwargs["data"] = data + + # 6. Wrap with py_extension_wrapper for PEP 3149 naming & PyInfo + py_extension_wrapper( name = name, - shared_lib_name = module_name + ".so", + src = ":" + csl_name, + visibility = visibility, **kwargs ) diff --git a/python/private/cc/py_extension_rule.bzl b/python/private/cc/py_extension_rule.bzl index 19a0281288..86fd801ace 100644 --- a/python/private/cc/py_extension_rule.bzl +++ b/python/private/cc/py_extension_rule.bzl @@ -1,129 +1,56 @@ -"""Implementation of the py_extension rule.""" +"""Implementation of the _py_extension_wrapper rule.""" -load("@rules_cc//cc/common:cc_common.bzl", "cc_common") -load("@rules_cc//cc/common:cc_info.bzl", "CcInfo") +load("@rules_cc//cc/common:cc_shared_library_info.bzl", "CcSharedLibraryInfo") +load("//python:versions.bzl", "PLATFORMS") load("//python/private:attr_builders.bzl", "attrb") load("//python/private:attributes.bzl", "COMMON_ATTRS") +load("//python/private:builders.bzl", "builders") load("//python/private:py_info.bzl", "PyInfo") -load("//python/private:py_internal.bzl", "py_internal") -load("//python/private:reexports.bzl", "BuiltinPyInfo") load("//python/private:rule_builders.bzl", "ruleb") -load("//python/private:toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") +load("//python/private:toolchain_types.bzl", "PY_CC_TOOLCHAIN_TYPE") -def _py_extension_impl(ctx): +def _py_extension_wrapper_impl(ctx): module_name = ctx.attr.module_name or ctx.label.name repo_name = ctx.label.workspace_name or ctx.workspace_name - import_path = repo_name + "/" + ctx.label.package - cc_toolchain = ctx.toolchains["@bazel_tools//tools/cpp:toolchain_type"].cc - feature_configuration = cc_common.configure_features( - ctx = ctx, - cc_toolchain = cc_toolchain, - ) - - # Collect CcInfo from all deps for compilation - static_deps_infos = [dep[CcInfo] for dep in ctx.attr.static_deps] - dynamic_deps_infos = [dep[CcSharedLibraryInfo] for dep in ctx.attr.dynamic_deps] - external_deps_infos = [dep[CcInfo] for dep in ctx.attr.external_deps] - - # Static deps are linked directly into the .so - static_cc_info = cc_common.merge_cc_infos( - cc_infos = static_deps_infos, - ) - - # Dynamic deps are linked as shared libraries - linker_inputs = [dep.linker_input for dep in dynamic_deps_infos] - dynamic_linking_context = cc_common.create_linking_context( - linker_inputs = depset(linker_inputs), - ) - - user_link_flags = [] - user_link_flags.append("-Wl,--export-dynamic-symbol=PyInit_{module_name}".format( - module_name = module_name, - )) - - # The PyInit symbol looks unused, so the linker optimizes it away. Telling it - # to treat it as undefined causes it to be retained. - user_link_flags.append("-Wl,--undefined=PyInit_{module_name}".format( - module_name = module_name, - )) - - if ctx.attr.external_deps: - user_link_flags.append("-Wl,--allow-shlib-undefined") + import_path = repo_name + if ctx.label.package: + import_path = repo_name + "/" + ctx.label.package + cc_toolchain = ctx.toolchains["@bazel_tools//tools/cpp:toolchain_type"].cc ext = _get_extension(cc_toolchain) - use_py_limited_api = ctx.attr.py_limited_api and ctx.attr.py_limited_api != "none" + use_py_limited_api = bool(ctx.attr.py_limited_api) if use_py_limited_api: - # check that all dependencies have compatible API versions, if defined - _check_limited_api_compatibility(ctx, ctx.attr.py_limited_api) - output_filename = "{module_name}.abi3.{ext}".format( - module_name=module_name, - ext=ext, + module_name = module_name, + ext = ext, ) else: - py_toolchain = ctx.toolchains[TARGET_TOOLCHAIN_TYPE] - py_runtime = py_toolchain.py3_runtime - cc_toolchain = ctx.toolchains["@bazel_tools//tools/cpp:toolchain_type"].cc - platform_tag = _get_platform(cc_toolchain) - output_filename = "{module_name}.{pyc_tag}{abi_flags}-{platform}.{ext}".format( + py_toolchain = ctx.toolchains[PY_CC_TOOLCHAIN_TYPE] + py_cc_toolchain = py_toolchain.py_cc_toolchain + platform_tag = _get_platform(ctx) + output_filename = "{module_name}.{abi_tag}-{platform}.{ext}".format( module_name = module_name, - pyc_tag = py_runtime.pyc_tag, # e.g. "cpython-311" - abi_flags = py_runtime.abi_flags, # e.g. "" or "d" - platform = platform_tag, # e.g. "x86_64-linux-gnu" - ext = "so", + abi_tag = py_cc_toolchain.abi_tag, + platform = platform_tag, + ext = ext, ) - py_dso = ctx.actions.declare_file(output_filename) - static_linking_context = static_cc_info.linking_context - linking_contexts = [ - static_linking_context, - dynamic_linking_context, - ] - - # Add target-level linkopts last so users can override. - user_link_flags.extend(ctx.attr.linkopts) - print(( - "===LINK:\n" + - " user_link_flags={user_link_flags}" - ).format( - user_link_flags = user_link_flags, - )) - - # todo: add linker script to hide symbols by default - # py_internal allows using some private apis, which may or may not be needed. - # based upon cc_shared_library.bzl - cc_linking_outputs = py_internal.link( - actions = ctx.actions, - feature_configuration = feature_configuration, - cc_toolchain = cc_toolchain, - linking_contexts = linking_contexts, - user_link_flags = user_link_flags, - # todo: add additional_inputs - name = ctx.label.name, - output_type = "dynamic_library", - main_output = py_dso, - # todo: maybe variables_extension - # todo: maybe additional_outputs - ) - print(( - "===LINK OUTPUT:\n" + - " {}" - ).format( - cc_linking_outputs, - )) + py_dso = ctx.actions.declare_file(output_filename) - # Propagate CcInfo from dynamic and external deps, but not static ones. - dynamic_cc_info = CcInfo(linking_context = dynamic_linking_context) - propagated_cc_info = cc_common.merge_cc_infos( - cc_infos = [dynamic_cc_info] + external_deps_infos, + # Symlink the cc_shared_library output to the PEP 3149 / abi3 filename + csl_target = ctx.attr.src + csl_file = csl_target[DefaultInfo].files.to_list()[0] + ctx.actions.symlink( + output = py_dso, + target_file = csl_file, ) - runfiles = ctx.runfiles(files = [py_dso]) - transitive_runfiles = [] - for dep in ctx.attr.static_deps + ctx.attr.dynamic_deps + ctx.attr.external_deps: - if DefaultInfo in dep: - transitive_runfiles.append(dep[DefaultInfo].default_runfiles) - runfiles = runfiles.merge_all(transitive_runfiles) + runfiles_builder = builders.RunfilesBuilder() + runfiles_builder.add(py_dso) + runfiles_builder.add(ctx.files.data) + runfiles_builder.add_targets(ctx.attr.data) + runfiles_builder.add(csl_target[DefaultInfo].default_runfiles) + runfiles = runfiles_builder.build(ctx) return [ DefaultInfo( @@ -134,66 +61,37 @@ def _py_extension_impl(ctx): transitive_sources = depset([py_dso]), imports = depset([import_path]), ), - propagated_cc_info, + csl_target[CcSharedLibraryInfo], ] -_MaybeBuiltinPyInfo = [[BuiltinPyInfo]] if BuiltinPyInfo != None else [] - -PY_EXTENSION_ATTRS = COMMON_ATTRS | { - "dynamic_deps": lambda: attrb.LabelList( - providers = [CcSharedLibraryInfo], - doc = "cc_shared_library targets to be dynamically linked.", - default = [], - ), - "external_deps": lambda: attrb.LabelList( - providers = [CcInfo], - doc = "cc_library targets with external linkage.", - default = [], - ), - "static_deps": lambda: attrb.LabelList( - providers = [CcInfo], - doc = "cc_library targets to be statically and privately linked.", - default = [], - ), - "copts": lambda: attrb.StringList(), - "linkopts": lambda: attrb.StringList(), +PY_EXTENSION_WRAPPER_ATTRS = COMMON_ATTRS | { + "libc": lambda: attrb.String(default = "glibc"), "module_name": lambda: attrb.String(), "py_limited_api": lambda: attrb.String( - doc = """\ - The minimum Python version to target for the Limited API (e.g., '3.8'). - - If set to a version string (e.g., '3.8') instead of 'none': - - Configures the output filename to use the simple '.abi3' suffix (e.g., - 'ext.abi3.so'). - - Strictly validates that all linked C++ dependencies (static_deps, - dynamic_deps, etc.) are binary-compatible with this target version, - failing the build if a dependency is missing the 'Py_LIMITED_API' define - or targets a newer version. - - Note: Since the py_extension rule only links pre-compiled libraries, you must - manually add the preprocessor macro to the cc_library targets that compile your - C/C++ sources, for example: - cc_library( - name = "my_impl", - srcs = ["my_code.c"], - defines = ["Py_LIMITED_API=0x03080000"], - ... - ) - - Set to 'none' (the default) to build a standard, version-specific extension. - """, - default = "none" + default = "", + ), + "src": lambda: attrb.Label( + mandatory = True, + providers = [CcSharedLibraryInfo], + doc = "The cc_shared_library target to wrap.", + ), + "_constraints": lambda: attrb.LabelList( + default = sorted({ + c: None + for info in PLATFORMS.values() + for c in info.compatible_with + }.keys()), ), } -def create_py_extension_rule_builder(**kwargs): - """Create a rule builder for a py_extension.""" +def create_py_extension_wrapper_rule_builder(**kwargs): + """Create a rule builder for the wrapper.""" builder = ruleb.Rule( - implementation = _py_extension_impl, - attrs = PY_EXTENSION_ATTRS, - provides = [PyInfo, CcInfo], + implementation = _py_extension_wrapper_impl, + attrs = PY_EXTENSION_WRAPPER_ATTRS, + provides = [PyInfo, CcSharedLibraryInfo], toolchains = [ - ruleb.ToolchainType(TARGET_TOOLCHAIN_TYPE), + ruleb.ToolchainType(PY_CC_TOOLCHAIN_TYPE), ruleb.ToolchainType("@bazel_tools//tools/cpp:toolchain_type"), ], fragments = ["cpp"], @@ -201,21 +99,7 @@ def create_py_extension_rule_builder(**kwargs): ) return builder -py_extension = create_py_extension_rule_builder().build() - -# Map Bazel's internal CPU names to PEP 3149 standard architecture names -_BAZEL_CPU_TO_PEP_ARCH = { - "k8": "x86_64", - "amd64": "x86_64", - "x86_64": "x86_64", - "aarch64": "aarch64", - "arm64": "arm64", - "darwin": "x86_64", # Historical Bazel Mac CPU - "darwin_x86_64": "x86_64", - "darwin_arm64": "arm64", - "x64_windows": "x86_64", - "arm64_windows": "arm64", -} +py_extension_wrapper = create_py_extension_wrapper_rule_builder().build() def _get_extension(cc_toolchain): """ @@ -235,161 +119,86 @@ def _get_extension(cc_toolchain): ext = "pyd" if is_windows else "so" return ext -def _get_platform(cc_toolchain): - """Derives the PEP 3149 platform tag from the C++ toolchain. - - Args: - cc_toolchain: The CcToolchainInfo provider (usually obtained via - ctx.toolchains["@bazel_tools//tools/cpp:toolchain_type"].cc) - - Returns: - The platform tag, e.g. "x86_64-linux-gnu" or "win_amd64" - """ - # Get the GNU target name (e.g., "local-linux-gnu" or "x86_64-unknown-linux-gnu") - target_name = cc_toolchain.target_gnu_system_name - - # Detect the OS family - is_windows = "windows" in target_name or "mingw" in target_name or "msvc" in target_name - is_mac = "apple" in target_name or "darwin" in target_name - - # Parse the architecture from the target_name - # e.g., "x86_64-unknown-linux-gnu" -> "x86_64" - target_parts = target_name.split("-") - arch = target_parts[0] - - # Handle the "local" placeholder by falling back to cc_toolchain.cpu - if arch == "local": - cpu = cc_toolchain.cpu - # Resolve the Bazel CPU name to a standard PEP architecture - arch = _BAZEL_CPU_TO_PEP_ARCH.get(cpu, cpu) - # Normalize standard names if they came from a full target_name - elif arch == "amd64": - arch = "x86_64" - elif arch == "aarch64": - arch = "arm64" if is_mac else "aarch64" - - # Derive the PEP 3149 / PEP 425 platform tag - if is_windows: - platform_tag = "win_amd64" if arch == "x86_64" else "win32" - elif is_mac: - platform_tag = "darwin" +def _derive_pep3149_tag(platform, info): + # platform is the triplet, e.g. "x86_64-unknown-linux-gnu" + p, _, _ = platform.partition("-freethreaded") + parts = p.split("-") + triplet_arch = parts[0] + + if info.os_name == "windows": + if triplet_arch == "x86_64": + return "win_amd64" + elif triplet_arch == "aarch64": + return "win_arm64" + else: + return "win32" + elif info.os_name == "osx": + return "darwin" + elif info.os_name == "linux": + abi = "musl" if p.endswith("-musl") else "gnu" + return "{}-linux-{}".format(triplet_arch, abi) else: - # Linux/Unix: Reconstruct the triplet, dropping the vendor if present - os_part = "linux" - abi_part = "gnu" - - if len(target_parts) == 4: - # [arch, vendor, os, abi] - os_part = target_parts[2] - abi_part = target_parts[3] - elif len(target_parts) == 3: - # [arch, os, abi] - os_part = target_parts[1] - abi_part = target_parts[2] - - platform_tag = "{}-{}-{}".format(arch, os_part, abi_part) - - return platform_tag - - -def _version_to_hex(version_str): - """Converts a version string like '3.10' to Python's version hex '0x030a0000'.""" - parts = version_str.split(".") - if len(parts) != 2: - fail("Invalid py_limited_api version '{}', expected 'major.minor' format (e.g., '3.8')".format(version_str)) - - major = int(parts[0]) - minor = int(parts[1]) - - if major != 3: - fail("Python Limited API is only supported for Python 3.2+ (got Python {})".format(major)) - if minor < 2: - fail("Python Limited API is only supported for Python 3.2+ (got 3.{})".format(minor)) - - # Format the minor version as a 2-digit hex (e.g., 10 -> "0a") - # Starlark doesn't seem to support %02x formatting - - return "0x03%x%x0000" % (int(minor/16), minor%16) - - -def _check_limited_api_compatibility(ctx, ext_version_str): - """Validates that all C++ dependencies are binary-compatible with the extension's Limited API target.""" - if ext_version_str == "none": - return - - ext_version_hex = _version_to_hex(ext_version_str) - ext_version_val = int(ext_version_hex, 16) - - # Collect all dependencies that might propagate CcInfo - deps = [] - deps.extend(ctx.attr.static_deps) - deps.extend(ctx.attr.dynamic_deps) - deps.extend(ctx.attr.external_deps) - - for dep in deps: - if CcInfo not in dep: - continue - - comp_ctx = dep[CcInfo].compilation_context - - # Detect if the dependency has access to Python headers - has_python_headers = False - for header in comp_ctx.headers.to_list(): - if header.basename == "Python.h": - has_python_headers = True + return triplet_arch + +def _get_platform_from_constraints(ctx): + # Build a map of Label to ConstraintValueInfo from _constraints + constraints_map = {} + for c in ctx.attr._constraints: + if platform_common.ConstraintValueInfo in c: + constraints_map[c.label] = c[platform_common.ConstraintValueInfo] + + # Resolve the target's libc to its config_setting label string + target_libc_setting = None + if ctx.attr.libc == "musl": + target_libc_setting = str(Label("//python/config_settings:_is_py_linux_libc_musl")) + elif ctx.attr.libc == "glibc": + target_libc_setting = str(Label("//python/config_settings:_is_py_linux_libc_glibc")) + + # Find the matching platform in PLATFORMS + for platform, info in PLATFORMS.items(): + # Check if all compatible_with constraints are satisfied + match = True + for c_str in info.compatible_with: + c_label = Label(c_str) + if c_label in constraints_map: + c_val = constraints_map[c_label] + if not ctx.target_platform_has_constraint(c_val): + match = False + break + else: + match = False break - # Inspect the propagated defines - has_limited_api_define = False - limited_api_define_value = None + if match: + # Additional check for Linux libc consistency using target_settings + if info.os_name == "linux" and target_libc_setting: + if target_libc_setting not in info.target_settings: + match = False - for define in comp_ctx.defines.to_list(): - if define.startswith("Py_LIMITED_API="): - has_limited_api_define = True - limited_api_define_value = define.split("=")[1] - elif define == "Py_LIMITED_API": - has_limited_api_define = True - limited_api_define_value = "unspecified" + if match: + return _derive_pep3149_tag(platform, info) - # Enforce the compatibility contract + return None - # Contract Rule A: If the library uses Python, it MUST use the Limited API - if has_python_headers and not has_limited_api_define: - fail(( - "\nERROR: Unsafe Python C API usage in dependency:\n" + - " Dependency '{dep}' includes Python headers (contains 'Python.h')\n" + - " but does NOT define 'Py_LIMITED_API'.\n" + - " This will link unstable Python symbols into your Stable ABI extension.\n" + - " Please add: defines = [\"Py_LIMITED_API={ext_hex}\"] to '{dep}'." - ).format( - dep = dep.label, - ext_hex = ext_version_hex, - )) +def _get_platform(ctx): + """Derives the PEP 3149 platform tag from the target constraints. - # Contract Rule B: If the Limited API is defined, it must be version-safe - if has_limited_api_define: - if limited_api_define_value == "unspecified": - fail(( - "\nERROR: Unsafe Python Limited API definition in dependency\n" + - " Dependency '{dep}' defines 'Py_LIMITED_API' without a version hex.\n" + - " Please change it to specify the target version explicitly, " + - "for example: defines = [\"Py_LIMITED_API={ext_hex}\"]" - ).format( - dep = dep.label, - ext_hex = ext_version_hex, - )) - else: - dep_version_val = int(limited_api_define_value, 16) - if dep_version_val > ext_version_val: - fail(( - "\nERROR: Incompatible Python Limited API targets detected\n" + - " Extension '{self}' targets version '{ext_ver}' ({ext_hex}).\n" + - " Dependency '{dep}' targets a NEWER version ({dep_hex}).\n" + - " You cannot link a newer Limited API library into an older extension." - ).format( - self = ctx.label, - ext_ver = ext_version_str, - ext_hex = ext_version_hex, - dep = dep.label, - dep_hex = limited_api_define_value, - )) + Args: + ctx: The rule context. + + Returns: + The platform tag, e.g. "x86_64-linux-gnu" or "win_amd64" + """ + platform_tag = _get_platform_from_constraints(ctx) + if platform_tag: + return platform_tag + + fail( + """ +ERROR: Unsupported target platform for {self}. + The target platform's constraints do not match any supported platform + in rules_python's central registry (python/versions.bzl). + Please ensure your target platform is configured correctly.""".format( + self = ctx.label, + ), + ) diff --git a/python/private/py_cc_toolchain_info.bzl b/python/private/py_cc_toolchain_info.bzl index 8cb3680b59..da7938503f 100644 --- a/python/private/py_cc_toolchain_info.bzl +++ b/python/private/py_cc_toolchain_info.bzl @@ -17,6 +17,11 @@ PyCcToolchainInfo = provider( doc = "C/C++ information about the Python runtime.", fields = { + "abi_tag": """\ +:type: str + +The ABI tag for extension modules, e.g. 'cpython-311' or 'cpython-313t'. +""", "headers": """\ :type: struct diff --git a/python/private/py_cc_toolchain_rule.bzl b/python/private/py_cc_toolchain_rule.bzl index b5c997ea6e..4067df668d 100644 --- a/python/private/py_cc_toolchain_rule.bzl +++ b/python/private/py_cc_toolchain_rule.bzl @@ -45,7 +45,14 @@ def _py_cc_toolchain_impl(ctx): else: headers_abi3 = None + abi_tag = ctx.attr.abi_tag + if not abi_tag: + # Derive default: cpython-XX + version_parts = ctx.attr.python_version.split(".") + abi_tag = "cpython-{}{}".format(version_parts[0], version_parts[1]) + py_cc_toolchain = PyCcToolchainInfo( + abi_tag = abi_tag, headers = struct( providers_map = { "CcInfo": ctx.attr.headers[CcInfo], @@ -67,6 +74,10 @@ def _py_cc_toolchain_impl(ctx): py_cc_toolchain = rule( implementation = _py_cc_toolchain_impl, attrs = { + "abi_tag": attr.string( + doc = "The ABI tag for extension modules, e.g. 'cpython-311'", + default = "", + ), "headers": attr.label( doc = ("Target that provides the Python headers. Typically this " + "is a cc_library target."), diff --git a/tests/cc/py_extension/BUILD.bazel b/tests/cc/py_extension/BUILD.bazel index e139798b24..65d37b73da 100644 --- a/tests/cc/py_extension/BUILD.bazel +++ b/tests/cc/py_extension/BUILD.bazel @@ -4,6 +4,7 @@ load("//python:py_test.bzl", "py_test") # buildifier: disable=bzl-visibility load("//python/cc:py_extension.bzl", "py_extension") +load(":dependency_graph_tests.bzl", "dependency_graph_test_suite") load(":py_extension_tests.bzl", "py_extension_analysis_test_suite") load(":py_limited_api_tests.bzl", "py_limited_api_test_suite") @@ -14,34 +15,47 @@ package( licenses(["notice"]) +##### + +py_extension( + # An extension defined solely by source files, with no deps + name = "ext_source", + srcs = ["ext_source.c"], +) + +##### + py_extension( + # A python extension that gets its code from a statically-linked library name = "ext_static", - ##srcs = ["ext_static.c"], - static_deps = [":static_dep"], + deps = [":static_dep"], ) +cc_library( + name = "static_dep", + srcs = ["static_dep.c"], + hdrs = ["static_dep.h"], +) + +##### + py_extension( - name = "ext_shared", - dynamic_deps = [ - ":add_one_shared", - ], - static_deps = [ - ":ext_shared_impl", - ], + # An extension that also depends on a data file + name = "ext_with_data", + data = ["test_symbols.h"], + deps = [":static_dep"], ) +##### + py_extension( - name = "ext_csl_shared", + # A python extension that dynamically links to another shared library + name = "ext_shared", dynamic_deps = [ ":add_one_shared", ], - exports_filter = [ - "//tests/cc/py_extension:ext_shared_impl", - ":ext_shared_impl", - ], - module_name = "ext_shared", - use_csl = True, deps = [ + # ":ext_shared_impl", ], ) @@ -63,12 +77,6 @@ cc_library( ], ) -cc_library( - name = "static_dep", - srcs = ["static_dep.c"], - hdrs = ["static_dep.h"], -) - cc_shared_library( name = "add_one_shared", deps = [":add_one_impl"], @@ -94,25 +102,67 @@ cc_library( hdrs = ["add_one_helper.h"], ) +##### + py_extension( + # An extension that uses the Python limited API name = "ext_limited", - static_deps = [":ext_limited_impl"], - py_limited_api = '3.8' + py_limited_api = "3.8", + deps = [":ext_limited_impl"], ) cc_library( name = "ext_limited_impl", srcs = ["ext_limited.c"], - defines = ["Py_LIMITED_API=0x3080000"], copts = [ "-fPIC", "-fvisibility=hidden", ], + defines = ["Py_LIMITED_API=0x3080000"], + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], +) + +##### + +py_extension( + # An extension with its PyInit_* function in a static dependency + name = "ext_init_in_dep", + deps = [":ext_init_in_dep_impl"], +) + +cc_library( + name = "ext_init_in_dep_impl", + srcs = ["ext_init_in_dep.c"], + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], +) + +##### + +py_extension( + # An extension with its PyInit_* function in a dynamic dependency + name = "ext_init_in_dynamic_dep", + dynamic_deps = [":ext_init_in_dynamic_dep_2"], +) + +cc_shared_library( + name = "ext_init_in_dynamic_dep_2", + deps = [":ext_init_in_dynamic_dep_impl"], +) + +cc_library( + name = "ext_init_in_dynamic_dep_impl", + srcs = ["ext_init_in_dynamic_dep.c"], deps = [ "@rules_python//python/cc:current_py_cc_headers", ], ) +##### + py_test( name = "py_extension_test", srcs = ["py_extension_test.py"], @@ -130,3 +180,7 @@ py_extension_analysis_test_suite( py_limited_api_test_suite( name = "py_limited_api_tests", ) + +dependency_graph_test_suite( + name = "dependency_graph_tests", +) diff --git a/tests/cc/py_extension/dependency_graph_tests.bzl b/tests/cc/py_extension/dependency_graph_tests.bzl new file mode 100644 index 0000000000..16ee883465 --- /dev/null +++ b/tests/cc/py_extension/dependency_graph_tests.bzl @@ -0,0 +1,296 @@ +# Copyright 2025 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Parity tests comparing cc_shared_library and py_extension behavior.""" + +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_shared_library.bzl", "cc_shared_library") +load("@rules_cc//cc/common:cc_shared_library_info.bzl", "CcSharedLibraryInfo") +load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite") +load("@rules_testing//lib:util.bzl", "util") +load("//python/cc:py_extension.bzl", "py_extension") + +# For tests 1 and 2 +def _create_dynamic_deps_helpers(name): + util.helper_target( + cc_library, + name = name + "_libC", + srcs = ["test_lib_c.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + ) + util.helper_target( + cc_shared_library, + name = name + "_cslC", + deps = [":" + name + "_libC"], + ) + util.helper_target( + cc_library, + name = name + "_libB", + srcs = ["test_lib_b.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + deps = [":" + name + "_libC"], + ) + util.helper_target( + cc_shared_library, + name = name + "_cslB", + deps = [":" + name + "_libB"], + dynamic_deps = [":" + name + "_cslC"], + ) + util.helper_target( + cc_library, + name = name + "_libA", + srcs = ["test_lib_a.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + deps = [":" + name + "_libB", ":" + name + "_libC"], + ) + +# Test 1: CSL A -> CSL B -> CSL C (Dynamic deps) +def _test_csl_dynamic_deps_top(name): + _create_dynamic_deps_helpers(name) + util.helper_target( + cc_shared_library, + name = name + "_cslA", + deps = [":" + name + "_libA"], + dynamic_deps = [":" + name + "_cslB", ":" + name + "_cslC"], + ) + analysis_test( + name = name, + target = name + "_cslA", + impl = _csl_dynamic_deps_test_impl, + ) + +def _csl_dynamic_deps_test_impl(env, target): + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + csl_info = target[CcSharedLibraryInfo] + + # Derive labels + test_name = target.label.name[:-5] # remove "_cslA" + lib_a_label = target.label.same_package_label(test_name + "_libA") + lib_b_label = target.label.same_package_label(test_name + "_libB") + lib_c_label = target.label.same_package_label(test_name + "_libC") + + env.expect.that_collection([str(e) for e in csl_info.exports]).contains_exactly([str(lib_a_label)]) + if hasattr(csl_info, "link_once_static_libs"): + static_libs = [str(lbl) for lbl in csl_info.link_once_static_libs] + env.expect.that_collection(static_libs).contains(str(lib_a_label)) + env.expect.that_collection(static_libs).contains_none_of([str(lib_b_label), str(lib_c_label)]) + +# Test 2: py_extension A -> CSL B -> CSL C (Dynamic deps) +def _test_pyext_dynamic_deps_top(name): + _create_dynamic_deps_helpers(name) + py_extension( + name = name + "_pyextA", + deps = [":" + name + "_libA"], + dynamic_deps = [":" + name + "_cslB", ":" + name + "_cslC"], + ) + analysis_test( + name = name, + target = name + "_pyextA", + impl = _pyext_dynamic_deps_test_impl, + ) + +def _test_pyext_dynamic_deps_cslB(name): + _create_dynamic_deps_helpers(name) + analysis_test( + name = name, + target = name + "_cslB", + impl = _cslB_deps_test_impl, + ) + +def _test_pyext_dynamic_deps_cslC(name): + _create_dynamic_deps_helpers(name) + analysis_test( + name = name, + target = name + "_cslC", + impl = _cslC_deps_test_impl, + ) + +def _cslC_deps_test_impl(env, target): + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + csl_info = target[CcSharedLibraryInfo] + + # Derive labels + test_name = target.label.name[:-5] # remove "_cslC" + lib_c_label = target.label.same_package_label(test_name + "_libC") + + env.expect.that_collection([str(e) for e in csl_info.exports]).contains_exactly([str(lib_c_label)]) + if hasattr(csl_info, "link_once_static_libs"): + env.expect.that_collection([str(lbl) for lbl in csl_info.link_once_static_libs]).contains_exactly([str(lib_c_label)]) + +def _cslB_deps_test_impl(env, target): + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + csl_info = target[CcSharedLibraryInfo] + + # Derive labels + test_name = target.label.name[:-5] # remove "_cslB" + lib_b_label = target.label.same_package_label(test_name + "_libB") + lib_c_label = target.label.same_package_label(test_name + "_libC") + csl_c_label = target.label.same_package_label(test_name + "_cslC") + + env.expect.that_collection([str(e) for e in csl_info.exports]).contains_exactly([str(lib_b_label)]) + if hasattr(csl_info, "link_once_static_libs"): + static_libs = [str(lbl) for lbl in csl_info.link_once_static_libs] + env.expect.that_collection(static_libs).contains(str(lib_b_label)) + env.expect.that_collection(static_libs).contains_none_of([str(lib_c_label)]) + + if hasattr(csl_info, "dynamic_deps"): + dynamic_deps = [str(d.linker_input.owner) for d in csl_info.dynamic_deps.to_list()] + env.expect.that_collection(dynamic_deps).contains(str(csl_c_label)) + +def _pyext_dynamic_deps_test_impl(env, target): + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + csl_info = target[CcSharedLibraryInfo] + + # Derive labels + test_name = target.label.name[:-7] # remove "_pyextA" + lib_a_label = target.label.same_package_label(test_name + "_libA") + lib_b_label = target.label.same_package_label(test_name + "_libB") + lib_c_label = target.label.same_package_label(test_name + "_libC") + + env.expect.that_collection([str(e) for e in csl_info.exports]).contains_exactly([str(lib_a_label)]) + if hasattr(csl_info, "link_once_static_libs"): + static_libs = [str(lbl) for lbl in csl_info.link_once_static_libs] + env.expect.that_collection(static_libs).contains(str(lib_a_label)) + env.expect.that_collection(static_libs).contains_none_of([str(lib_b_label), str(lib_c_label)]) + +# For tests 3 and 4 +def _create_static_sharing_helpers(name): + util.helper_target( + cc_library, + name = name + "_libC", + srcs = ["test_lib_c.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + ) + util.helper_target( + cc_library, + name = name + "_libB", + srcs = ["test_lib_b.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + deps = [":" + name + "_libC"], + ) + util.helper_target( + cc_shared_library, + name = name + "_cslB", + deps = [":" + name + "_libB", ":" + name + "_libC"], + ) + util.helper_target( + cc_library, + name = name + "_libA", + srcs = ["test_lib_a.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + deps = [":" + name + "_libB", ":" + name + "_libC"], + ) + +# Test 3: CSL A -> CSL B, CL C (Static sharing) +def _test_csl_static_sharing_top(name): + _create_static_sharing_helpers(name) + util.helper_target( + cc_shared_library, + name = name + "_cslA", + deps = [":" + name + "_libA"], + dynamic_deps = [":" + name + "_cslB"], + ) + analysis_test( + name = name, + target = name + "_cslA", + impl = _csl_static_sharing_test_impl, + ) + +def _csl_static_sharing_test_impl(env, target): + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + csl_info = target[CcSharedLibraryInfo] + + # Derive labels + test_name = target.label.name[:-5] # remove "_cslA" + lib_a_label = target.label.same_package_label(test_name + "_libA") + lib_b_label = target.label.same_package_label(test_name + "_libB") + lib_c_label = target.label.same_package_label(test_name + "_libC") + + env.expect.that_collection([str(e) for e in csl_info.exports]).contains_exactly([str(lib_a_label)]) + if hasattr(csl_info, "link_once_static_libs"): + static_libs = [str(lbl) for lbl in csl_info.link_once_static_libs] + env.expect.that_collection(static_libs).contains(str(lib_a_label)) + env.expect.that_collection(static_libs).contains_none_of([str(lib_b_label), str(lib_c_label)]) + +# Test 4: Same as 3, but A is py_extension +def _test_pyext_static_sharing_top(name): + _create_static_sharing_helpers(name) + py_extension( + name = name + "_pyextA", + deps = [":" + name + "_libA"], + dynamic_deps = [":" + name + "_cslB"], + ) + analysis_test( + name = name, + target = name + "_pyextA", + impl = _pyext_static_sharing_test_impl, + ) + +def _test_pyext_static_sharing_cslB(name): + _create_static_sharing_helpers(name) + analysis_test( + name = name, + target = name + "_cslB", + impl = _cslB_static_sharing_test_impl, + ) + +def _cslB_static_sharing_test_impl(env, target): + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + csl_info = target[CcSharedLibraryInfo] + + # Derive labels + test_name = target.label.name[:-5] # remove "_cslB" + lib_b_label = target.label.same_package_label(test_name + "_libB") + lib_c_label = target.label.same_package_label(test_name + "_libC") + + env.expect.that_collection([str(e) for e in csl_info.exports]).contains_exactly([str(lib_b_label), str(lib_c_label)]) + if hasattr(csl_info, "link_once_static_libs"): + static_libs = [str(lbl) for lbl in csl_info.link_once_static_libs] + env.expect.that_collection(static_libs).contains_exactly([str(lib_b_label), str(lib_c_label)]) + +def _pyext_static_sharing_test_impl(env, target): + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + csl_info = target[CcSharedLibraryInfo] + + # Derive labels + test_name = target.label.name[:-7] # remove "_pyextA" + lib_a_label = target.label.same_package_label(test_name + "_libA") + lib_b_label = target.label.same_package_label(test_name + "_libB") + lib_c_label = target.label.same_package_label(test_name + "_libC") + + env.expect.that_collection([str(e) for e in csl_info.exports]).contains_exactly([str(lib_a_label)]) + if hasattr(csl_info, "link_once_static_libs"): + static_libs = [str(lbl) for lbl in csl_info.link_once_static_libs] + env.expect.that_collection(static_libs).contains(str(lib_a_label)) + env.expect.that_collection(static_libs).contains_none_of([str(lib_b_label), str(lib_c_label)]) + +def dependency_graph_test_suite(name): + test_suite( + name = name, + tests = [ + _test_csl_dynamic_deps_top, + _test_pyext_dynamic_deps_top, + _test_pyext_dynamic_deps_cslB, + _test_pyext_dynamic_deps_cslC, + _test_csl_static_sharing_top, + _test_pyext_static_sharing_top, + _test_pyext_static_sharing_cslB, + ], + ) diff --git a/tests/cc/py_extension/ext_init_in_dep.c b/tests/cc/py_extension/ext_init_in_dep.c new file mode 100644 index 0000000000..a6d32ba0b2 --- /dev/null +++ b/tests/cc/py_extension/ext_init_in_dep.c @@ -0,0 +1,20 @@ + +#include + +// No methods defined; we're just testing the init function. +static PyMethodDef ModuleMethods[] = { + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +static struct PyModuleDef ext_init_in_dep_module = { + PyModuleDef_HEAD_INIT, + "ext_init_in_dep", /* name of module */ + NULL, /* module documentation, may be NULL */ + -1, /* size of per-interpreter state of the module, + or -1 if the module keeps state in global variables. */ + ModuleMethods +}; + +PyMODINIT_FUNC PyInit_ext_init_in_dep(void) { + return PyModule_Create(&ext_init_in_dep_module); +} diff --git a/tests/cc/py_extension/ext_init_in_dynamic_dep.c b/tests/cc/py_extension/ext_init_in_dynamic_dep.c new file mode 100644 index 0000000000..ad196adb06 --- /dev/null +++ b/tests/cc/py_extension/ext_init_in_dynamic_dep.c @@ -0,0 +1,20 @@ + +#include + +// No methods defined; we're just testing the init function. +static PyMethodDef ModuleMethods[] = { + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +static struct PyModuleDef ext_init_in_dynamic_dep_module = { + PyModuleDef_HEAD_INIT, + "ext_init_in_dynamic_dep", /* name of module */ + NULL, /* module documentation, may be NULL */ + -1, /* size of per-interpreter state of the module, + or -1 if the module keeps state in global variables. */ + ModuleMethods +}; + +PyMODINIT_FUNC PyInit_ext_init_in_dynamic_dep(void) { + return PyModule_Create(&ext_init_in_dynamic_dep_module); +} diff --git a/tests/cc/py_extension/ext_source.c b/tests/cc/py_extension/ext_source.c new file mode 100644 index 0000000000..df0239df3a --- /dev/null +++ b/tests/cc/py_extension/ext_source.c @@ -0,0 +1,33 @@ + +#include + + +static PyObject* calc_one_plus_two(PyObject* self, PyObject* args) { + return PyLong_FromLong(1 + 2); +} + + +// Method definition object for this extension, these are the functions +// that will be available in the module. +static PyMethodDef ModuleMethods[] = { + {"calc_one_plus_two", calc_one_plus_two, METH_NOARGS, "A simple C function."}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +// Module definition +// The arguments of this structure tell Python what to call your extension, +// what its methods are and where to look for its method definitions. +static struct PyModuleDef ext_source_module = { + PyModuleDef_HEAD_INIT, + "ext_source", /* name of module */ + NULL, /* module documentation, may be NULL */ + -1, /* size of per-interpreter state of the module, + or -1 if the module keeps state in global variables. */ + ModuleMethods +}; + +// The module init function. This must be exported and retained in the +// shared library output. +PyMODINIT_FUNC PyInit_ext_source(void) { + return PyModule_Create(&ext_source_module); +} diff --git a/tests/cc/py_extension/py_extension_test.py b/tests/cc/py_extension/py_extension_test.py index 252098a46a..52d5502ea7 100644 --- a/tests/cc/py_extension/py_extension_test.py +++ b/tests/cc/py_extension/py_extension_test.py @@ -11,7 +11,10 @@ class PyExtensionTest(unittest.TestCase): def test_inspect_elf(self): r = runfiles.Create() - ext_path = r.Rlocation("rules_python/tests/cc/py_extension/ext_shared.so") + ext_path = r.Rlocation( + "rules_python/tests/cc/py_extension/" + + "ext_shared.cpython-311-x86_64-linux-gnu.so" + ) self.assertTrue( os.path.exists(ext_path), f"Could not find ext_shared.so at {ext_path}" ) diff --git a/tests/cc/py_extension/py_extension_tests.bzl b/tests/cc/py_extension/py_extension_tests.bzl index 514bb22ac4..13fb4e9e0c 100644 --- a/tests/cc/py_extension/py_extension_tests.bzl +++ b/tests/cc/py_extension/py_extension_tests.bzl @@ -14,18 +14,17 @@ """Tests for py_extension.""" -load("@rules_cc//cc/common:cc_info.bzl", "CcInfo") +load("@rules_cc//cc/common:cc_shared_library_info.bzl", "CcSharedLibraryInfo") load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite") load("@rules_testing//lib:truth.bzl", "matching") -load("//python/private:py_info.bzl", "PyInfo") +load("//python/private:py_info.bzl", "PyInfo") # buildifier: disable=bzl-visibility _tests = [] def _test_static_deps_impl(env, target): env.expect.that_target(target).has_provider(PyInfo) py_info = target[PyInfo] - env.expect.that_target(target).has_provider(CcInfo) - cc_info = target[CcInfo] + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) # The .so should be in PyInfo env.expect.that_collection(py_info.transitive_sources.to_list()).has_size(1) @@ -33,9 +32,6 @@ def _test_static_deps_impl(env, target): matching.file_basename_equals("ext_static.cpython-311-x86_64-linux-gnu.so"), ) - # CcInfo from static_deps should not be propagated. - env.expect.that_depset_of_files(cc_info.linking_context.linker_inputs).contains_exactly([]) - def _test_static_deps(name): analysis_test( name = name, @@ -45,11 +41,29 @@ def _test_static_deps(name): _tests.append(_test_static_deps) +def _test_data_deps_impl(env, target): + env.expect.that_target(target).has_provider(PyInfo) + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + + # Check that data file is in runfiles + default_info = target[DefaultInfo] + env.expect.that_depset_of_files(default_info.default_runfiles.files).contains_predicate( + matching.file_basename_equals("test_symbols.h"), + ) + +def _test_data_deps(name): + analysis_test( + name = name, + impl = _test_data_deps_impl, + target = "//tests/cc/py_extension:ext_with_data", + ) + +_tests.append(_test_data_deps) + def _test_dynamic_deps_impl(env, target): env.expect.that_target(target).has_provider(PyInfo) py_info = target[PyInfo] - env.expect.that_target(target).has_provider(CcInfo) - cc_info = target[CcInfo] + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) # The .so should be in PyInfo env.expect.that_collection(py_info.transitive_sources.to_list()).has_size(1) @@ -57,9 +71,8 @@ def _test_dynamic_deps_impl(env, target): matching.file_basename_equals("ext_shared.cpython-311-x86_64-linux-gnu.so"), ) - # CcInfo from dynamic_deps should be propagated. - print(cc_info.linking_context.linker_inputs.to_list()) - env.expect.that_collection(cc_info.linking_context.linker_inputs.to_list()).has_size(1) + # CcSharedLibraryInfo provider should be present and non-empty + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) def _test_dynamic_deps(name): analysis_test( @@ -70,6 +83,25 @@ def _test_dynamic_deps(name): _tests.append(_test_dynamic_deps) +def _test_musl_platform_impl(env, target): + env.expect.that_target(target).has_provider(PyInfo) + py_info = target[PyInfo] + env.expect.that_depset_of_files(py_info.transitive_sources).contains_predicate( + matching.file_basename_equals("ext_static.cpython-311-x86_64-linux-musl.so"), + ) + +def _test_musl_platform(name): + analysis_test( + name = name, + impl = _test_musl_platform_impl, + target = "//tests/cc/py_extension:ext_static", + config_settings = { + str(Label("//python/config_settings:py_linux_libc")): "musl", + }, + ) + +_tests.append(_test_musl_platform) + def py_extension_analysis_test_suite(name): test_suite( name = name, diff --git a/tests/cc/py_extension/py_limited_api_tests.bzl b/tests/cc/py_extension/py_limited_api_tests.bzl index 5df7892535..a59628c19c 100644 --- a/tests/cc/py_extension/py_limited_api_tests.bzl +++ b/tests/cc/py_extension/py_limited_api_tests.bzl @@ -14,64 +14,126 @@ """Tests for the py_limited_api attribute for py_extension.""" +load("@rules_cc//cc:cc_library.bzl", "cc_library") load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite") -load("@rules_testing//lib:truth.bzl", "matching") load("@rules_testing//lib:util.bzl", "util") load("//python/cc:py_extension.bzl", "py_extension") -load("@rules_cc//cc:cc_library.bzl", "cc_library") +def _test_limited_pass_impl(env, target): + env.expect.that_target(target).default_outputs().contains( + "tests/cc/py_extension/{}.abi3.so".format(target.label.name), + ) def _test_limited_same_version(name): - # given util.helper_target( cc_library, - name = name + '_csl', - defines = ["Py_LIMITED_API=0x3080000"], + name = name + "_csl", + defines = ["Py_LIMITED_API=0x03080000"], deps = [ "@rules_python//python/cc:current_py_cc_headers", ], ) py_extension( - name = name + '_pyext', - static_deps = [':' + name + '_csl'], - py_limited_api = '3.8', + name = name + "_pyext", + deps = [":" + name + "_csl"], + py_limited_api = "3.8", + ) + analysis_test( + name = name, + target = name + "_pyext", + impl = _test_limited_pass_impl, ) - # when +def _test_limited_older_dep(name): + util.helper_target( + cc_library, + name = name + "_csl", + defines = ["Py_LIMITED_API=0x03080000"], # 3.8 + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], + ) + py_extension( + name = name + "_pyext", + deps = [":" + name + "_csl"], + py_limited_api = "3.9", # 3.9 + ) analysis_test( name = name, target = name + "_pyext", - impl=_test_limited_same_version_impl) + impl = _test_limited_pass_impl, + ) -def _test_limited_same_version_impl(env, target): - # then - env.expect.that_target(target).default_outputs().contains( - "tests/cc/py_extension/test_limited_same_version_pyext.abi3.so" +def _test_no_limited_api(name): + util.helper_target( + cc_library, + name = name + "_csl", + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], + ) + py_extension( + name = name + "_pyext", + deps = [":" + name + "_csl"], + ) + analysis_test( + name = name, + target = name + "_pyext", + impl = _test_no_limited_api_impl, + ) + +def _test_no_limited_api_impl(env, target): + # Should pass, nothing to assert on filename since it is platform-specific + _ = env # @unused + _ = target # @unused + +def _test_no_limited_api_dep_has_limited(name): + util.helper_target( + cc_library, + name = name + "_csl", + defines = ["Py_LIMITED_API=0x03080000"], + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], + ) + py_extension( + name = name + "_pyext", + deps = [":" + name + "_csl"], + ) + analysis_test( + name = name, + target = name + "_pyext", + impl = _test_no_limited_api_dep_has_limited_impl, ) -# test cases: -# py_limited_api -# - 3.8 -> 3.9 -# - 3.9 -> 3.8 -# - 3.9 -> 3.9 ok -# - none -> 3.8 -# - 3.8 -> none fail -# - 3.8 -> nopy ok -# - none -> none ok? -# - none -> nopy ok? -# invalid values for version string -# - 2.x -# - 3.0 and 3.1 -# - 4.x -# - not version string, e.g. "asdf" -# - patch versions? 3.8.4 ? -# - empty string or null? +def _test_no_limited_api_dep_has_limited_impl(env, target): + _ = env # @unused + _ = target # @unused +def _test_limited_api_dep_has_no_python(name): + util.helper_target( + cc_library, + name = name + "_csl", + ) + py_extension( + name = name + "_pyext", + deps = [":" + name + "_csl"], + py_limited_api = "3.8", + ) + analysis_test( + name = name, + target = name + "_pyext", + impl = _test_limited_pass_impl, + ) def py_limited_api_test_suite(name): test_suite( name = name, tests = [ _test_limited_same_version, + _test_limited_older_dep, + _test_no_limited_api, + _test_no_limited_api_dep_has_limited, + _test_limited_api_dep_has_no_python, ], ) diff --git a/tests/cc/py_extension/static_dep.c b/tests/cc/py_extension/static_dep.c index 4d910c1178..fa95e4dacc 100644 --- a/tests/cc/py_extension/static_dep.c +++ b/tests/cc/py_extension/static_dep.c @@ -1,4 +1,4 @@ -#include "my_lib.h" +#include "static_dep.h" int my_lib_func() { return 42; diff --git a/tests/cc/py_extension/test_lib_a.c b/tests/cc/py_extension/test_lib_a.c new file mode 100644 index 0000000000..19e2f489bf --- /dev/null +++ b/tests/cc/py_extension/test_lib_a.c @@ -0,0 +1,6 @@ +#include "test_symbols.h" + +void fnA() { + fnB(); + fnC(); +} diff --git a/tests/cc/py_extension/test_lib_b.c b/tests/cc/py_extension/test_lib_b.c new file mode 100644 index 0000000000..3621587c1c --- /dev/null +++ b/tests/cc/py_extension/test_lib_b.c @@ -0,0 +1,5 @@ +#include "test_symbols.h" + +void fnB() { + fnC(); +} diff --git a/tests/cc/py_extension/test_lib_c.c b/tests/cc/py_extension/test_lib_c.c new file mode 100644 index 0000000000..99941576dd --- /dev/null +++ b/tests/cc/py_extension/test_lib_c.c @@ -0,0 +1,6 @@ +#include "test_symbols.h" +#include + +void fnC() { + printf("fnC\n"); +} diff --git a/tests/cc/py_extension/test_symbols.h b/tests/cc/py_extension/test_symbols.h new file mode 100644 index 0000000000..59ab3b02e7 --- /dev/null +++ b/tests/cc/py_extension/test_symbols.h @@ -0,0 +1,8 @@ +#ifndef TEST_SYMBOLS_H +#define TEST_SYMBOLS_H + +void fnC(); +void fnB(); +void fnA(); + +#endif // TEST_SYMBOLS_H