| :::{default-domain} bzl |
| ::: |
| |
| # How to optionally load symbols using `features.loadable_symbols` |
| |
| When writing Bazel rules, macros, or repository extensions that support |
| multiple versions of `rules_python`, you may want to detect whether a public |
| symbol (such as {obj}`py_extension` in `//python/cc:py_extension.bzl`) is |
| available before attempting to load or use it. |
| |
| Because Starlark `load()` statements are evaluated at parse time and must be at |
| the top level of a `.bzl` file, unconditionally loading a symbol that does not |
| exist in older versions of `rules_python` will cause a build error. |
| |
| The {bzl:obj}`features.loadable_symbols` dictionary in `//python:features.bzl` |
| allows you to programmatically inspect which symbols are exported by `.bzl` |
| files in the current `rules_python` version. |
| |
| ## The `features.loadable_symbols` structure |
| |
| {bzl:obj}`features.loadable_symbols` is a `dict[str, list[str]]` mapping label |
| strings of `.bzl` files to the list of public symbols they export: |
| |
| ```starlark |
| load("@rules_python//python:features.bzl", "features") |
| |
| # Example structure of features.loadable_symbols: |
| # { |
| # "//python/cc:py_extension.bzl": [ |
| # "py_extension", |
| # ], |
| # "//python:py_info.bzl": [ |
| # "PyInfo", |
| # ], |
| # } |
| ``` |
| |
| ## Using load() with optional symbols |
| |
| In repository rules or Bazel module extensions (`repository_ctx` or |
| `module_ctx`), you generate `.bzl` files dynamically. You can inspect |
| `features.loadable_symbols` to determine which `load()` statements to write into |
| a generated compatibility repository. |
| |
| Re-export the symbol under its standard name if available, or set it to `None` |
| if it is absent. By generating compatibility files and empty `BUILD.bazel` |
| files at the exact same relative package paths as `rules_python`, the only |
| difference in downstream `load()` statements is the repository name: |
| |
| ```starlark |
| load("@rules_python//python:features.bzl", "features") |
| |
| def _rules_python_compat_impl(rctx): |
| for bzl, symbol_list in rctx.attr.symbols.items(): |
| loadable = features.loadable_symbols.get(bzl, []) |
| lines = [] |
| for symbol in symbol_list: |
| if symbol in loadable: |
| lines.append( |
| 'load("{}", _{} = "{}")'.format(bzl, symbol, symbol), |
| ) |
| lines.append("{} = _{}".format(symbol, symbol)) |
| else: |
| lines.append("{} = None".format(symbol)) |
| |
| package, _, filename = bzl.lstrip("/").partition(":") |
| path = package + "/" + filename if package else filename |
| build_path = package + "/BUILD.bazel" if package else "BUILD.bazel" |
| |
| rctx.file(path, content = "\n".join(lines) + "\n") |
| rctx.file(build_path, content = "") |
| |
| rules_python_compat = repository_rule( |
| implementation = _rules_python_compat_impl, |
| attrs = { |
| "symbols": attr.string_list_dict( |
| mandatory = True, |
| doc = "Map of bzl paths to lists of symbols to optionally load", |
| ), |
| }, |
| ) |
| ``` |
| |
| Instantiate the repository rule by providing a mapping of `.bzl` paths to their |
| symbols of interest: |
| |
| ```starlark |
| rules_python_compat( |
| name = "rules_python_compat", |
| symbols = { |
| "//python/cc:py_extension.bzl": ["py_extension"], |
| }, |
| ) |
| ``` |
| |
| ### Using the generated compatibility files |
| |
| Your macros and rules can load from `@rules_python_compat` using the same |
| file path as `@rules_python`, testing whether the symbol is `None` before |
| using it: |
| |
| ```starlark |
| load("@rules_python_compat//python/cc:py_extension.bzl", "py_extension") |
| |
| def my_macro(name, **kwargs): |
| if py_extension != None: |
| py_extension( |
| name = name + "_ext", |
| **kwargs |
| ) |
| else: |
| # Fall back to default behavior for older rules_python versions |
| pass |
| ``` |
| |
| ## Handling optional targets |
| |
| In addition to symbol loading, you may need to check whether a specific Bazel |
| target exists in `rules_python` before referencing its label in dependencies, |
| toolchains, or attribute defaults. |
| |
| The {bzl:obj}`features.targets` dictionary in `//python:features.bzl` is a |
| `dict[str, bool]` mapping public API target labels to `True` when available. |
| |
| In a macro: |
| |
| ```starlark |
| load("@rules_python//python:features.bzl", "features") |
| |
| def my_cc_extension_macro(name, deps = [], **kwargs): |
| if features.targets.get("//python/cc:current_py_cc_headers_abi3"): |
| deps = deps + ["@rules_python//python/cc:current_py_cc_headers_abi3"] |
| |
| # ... define target with deps |
| ``` |
| |
| In a `BUILD` file: |
| |
| ```starlark |
| load("@rules_python//python:features.bzl", "features") |
| load("@rules_python//python:py_library.bzl", "py_library") |
| |
| py_library( |
| name = "my_lib", |
| srcs = ["my_lib.py"], |
| deps = [ |
| "//my/app:base_lib", |
| ] + ( |
| ["@rules_python//python/cc:current_py_cc_headers_abi3"] |
| if features.targets.get("//python/cc:current_py_cc_headers_abi3") |
| else [] |
| ), |
| ) |
| ``` |
| |
| ## Checking versions with `features.version` |
| |
| When a behavioral change or capability is not directly reflected by a public |
| target or loadable symbol, you can inspect {bzl:obj}`features.version` in |
| `//python:features.bzl`. |
| |
| {bzl:obj}`features.version` returns a semver-formatted version string (such as |
| `"1.0.0"`, `"2.0.0-rc2"`, or `""` for unreleased development builds): |
| |
| ```starlark |
| load("@rules_python//python:features.bzl", "features") |
| |
| def _to_tuple(v): |
| return tuple([ |
| int(x) if x.isdigit() else x |
| for x in v.replace("-", ".").split(".") |
| ]) |
| |
| def has_foo(): |
| # If version is empty, it is an unreleased build from main which includes |
| # all features. |
| if not features.version: |
| return True |
| return _to_tuple(features.version) >= _to_tuple("0.38.0") |
| ``` |