From eb2978b0e63aede343374d16d23f44adccc9f2e9 Mon Sep 17 00:00:00 2001 From: Kristian Hartikainen Date: Sat, 15 Aug 2026 02:00:57 -0400 Subject: [PATCH] Support source-less wheels with dependencies `rules_python` `2.3.x` wraps source-less wheels in a `py_library` whose `srcs` target has no Python files. Forward the target only when it has valid sources, and cover the wrapper behavior with unit tests. --- news/sourceless-wheel-srcs.fixed.md | 2 + python/private/pypi/whl_library_targets.bzl | 37 ++++-- .../whl_library_targets_tests.bzl | 116 ++++++++++++++++++ 3 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 news/sourceless-wheel-srcs.fixed.md diff --git a/news/sourceless-wheel-srcs.fixed.md b/news/sourceless-wheel-srcs.fixed.md new file mode 100644 index 0000000000..412c76bbf2 --- /dev/null +++ b/news/sourceless-wheel-srcs.fixed.md @@ -0,0 +1,2 @@ +(pypi) Fixed analysis failures for source-less wheels with dependencies in +per-wheel repositories. diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl index b7fdbd55e9..92b463d68b 100644 --- a/python/private/pypi/whl_library_targets.bzl +++ b/python/private/pypi/whl_library_targets.bzl @@ -101,7 +101,7 @@ def whl_library_targets( **kwargs: Extra args passed to the {obj}`whl_library_deps_targets` and {obj}`whl_library_srcs`. """ create_extra_targets = bool(requires_dist or group_name) and dep_template - whl_library_srcs( + wrapper_srcs = whl_library_srcs( name = name, sdist_filename = sdist_filename, data_exclude = data_exclude, @@ -133,6 +133,7 @@ def whl_library_targets( dep_template = dep_template, # only needed if requires_dist is present repo = None, # set aliases in the same repo aliases = {}, + srcs = wrapper_srcs, **kwargs ) @@ -193,9 +194,14 @@ def whl_library_srcs( pkg_name: {type}`str` The label name to use for the py_library target. native: {type}`native` The native struct for overriding in tests. rules: {type}`struct` A struct with references to rules for creating targets. + + Returns: + The source labels attached to the generated `py_library`, or `None` when + `rules` does not define `py_library`. """ tags = sorted(tags) data = [] + data + wrapper_srcs = None bins_for_data_label = [] @@ -301,6 +307,7 @@ def whl_library_srcs( # pure-Python code, e.g. pymssql, which is written in Cython. allow_empty = True, ) + wrapper_srcs = [pkg_name] if srcs else [] # NOTE: pyi files should probably be excluded because they're carried # by the pyi_srcs attribute. However, historical behavior included @@ -328,13 +335,19 @@ def whl_library_srcs( ) if not enable_implicit_namespace_pkgs: + generated_namespace_package_files = rules.create_inits( + srcs = srcs + data + pyi_srcs, + ignored_dirnames = [], # If you need to ignore certain folders, you can patch rules_python here to do so. + root = "site-packages", + ) + if not wrapper_srcs and generated_namespace_package_files: + wrapper_srcs = select({ + _IS_VENV_SITE_PACKAGES_YES: [], + "//conditions:default": [pkg_name], + }) generated_namespace_package_files = select({ _IS_VENV_SITE_PACKAGES_YES: [], - "//conditions:default": rules.create_inits( - srcs = srcs + data + pyi_srcs, - ignored_dirnames = [], # If you need to ignore certain folders, you can patch rules_python here to do so. - root = "site-packages", - ), + "//conditions:default": generated_namespace_package_files, }) namespace_package_files += generated_namespace_package_files srcs = srcs + generated_namespace_package_files @@ -356,6 +369,7 @@ def whl_library_srcs( experimental_venvs_site_packages = _VENV_SITE_PACKAGES_FLAG, namespace_package_files = namespace_package_files, ) + return wrapper_srcs def whl_library_deps_targets( *, @@ -369,6 +383,7 @@ def whl_library_deps_targets( group_deps = [], group_name = None, dep_template, + srcs = None, tags = [], visibility = ["//visibility:public"], native = native, @@ -396,6 +411,9 @@ def whl_library_deps_targets( include: {type}`list[str]` The list of packages to include. group_name: {type}`str | None` name of the dependency group (if any). dep_template: {type}`str | None` The dep_template to use. + srcs: {type}`list[Label] | None` or a configurable expression with the + source labels attached to the wrapper `py_library`. If `None`, the + source library target is used. tags: {type}`list[str]` The tags set on the targets. repo: {type}`str | Label | None` The BUILD.bazel label to the parent repo that has the sources. If none, then will take the targets from the current dir. @@ -501,10 +519,13 @@ def whl_library_deps_targets( ) if hasattr(rules, "py_library"): + if srcs == None: + srcs = [repo_label(PY_SRCS_LABEL)] rules.py_library( name = py_library_label, - # We include as srcs to ensure that the (locations :pkg) works as expected. - srcs = [repo_label(PY_SRCS_LABEL)], + # Forward source-producing targets through `srcs` so downstream + # `$(locations :pkg)` expansion does not reject source-less wheels. + srcs = srcs, deps = _deps( # We include as deps, so that `PyInfo` and friends (e.g. `pyi_srcs`) get # propagated. Just passing the target as `srcs` is not enough to propagate diff --git a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl index 3fe1b99768..3484f0b8e1 100644 --- a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl +++ b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl @@ -19,11 +19,46 @@ load( "//python/private/pypi:whl_library_targets.bzl", "whl_library_deps_targets", "whl_library_srcs", + "whl_library_targets", ) # buildifier: disable=bzl-visibility load("//tests/support/mocks:mocks.bzl", "mocks") _tests = [] +def _make_whl_library_targets_py_library_calls( + *, + srcs, + data, + generated_inits, + enable_implicit_namespace_pkgs = False): + py_library_calls = [] + m_glob = mocks.glob() + m_glob.results.append([]) # bin + m_glob.results.append([]) # rewrite-bin + m_glob.results.append([]) # rewrite-record + m_glob.results.append(srcs) + m_glob.results.append(data) + m_glob.results.append([]) # pyi + + whl_library_targets( + name = "foo-0-py3-none-any.whl", + metadata_name = "Foo", + requires_dist = ["bar"], + dep_template = "@pypi//{name}:{target}", + enable_implicit_namespace_pkgs = enable_implicit_namespace_pkgs, + filegroups = {}, + native = struct(glob = m_glob.glob), + rules = struct( + create_inits = lambda **_: generated_inits, + env_marker_setting = lambda **_: None, + gen_wheel_record = lambda **_: None, + py_library = lambda **kwargs: py_library_calls.append(kwargs), + venv_rewrite_shebang = lambda **_: None, + ), + ) + + return py_library_calls + def _test_filegroups(env): calls = [] @@ -191,6 +226,87 @@ def _test_whl_library_deps_targets(env): _tests.append(_test_whl_library_deps_targets) +def _test_whl_library_targets_sourceless(env): + for enable_implicit_namespace_pkgs, expected_leaf_srcs in [ + (False, [] + select({ + Label("//python/config_settings:_is_venvs_site_packages_yes"): [], + "//conditions:default": [], + })), + (True, []), + ]: + py_library_calls = _make_whl_library_targets_py_library_calls( + srcs = [], + data = [], + generated_inits = [], + enable_implicit_namespace_pkgs = enable_implicit_namespace_pkgs, + ) + + env.expect.that_collection(py_library_calls).has_size(2) + if len(py_library_calls) != 2: + return + + env.expect.that_dict(py_library_calls[0]).contains_at_least({ + "srcs": expected_leaf_srcs, + }) + env.expect.that_dict(py_library_calls[1]).contains_exactly({ + "name": "pkg", + "srcs": [], + "deps": ["srcs", "@pypi//bar:pkg"], + "tags": [], + "visibility": ["//visibility:public"], + }) # buildifier: @unsorted-dict-items + +_tests.append(_test_whl_library_targets_sourceless) + +def _test_whl_library_targets_sourceful(env): + for srcs, data, generated_inits, expected_leaf_srcs, expected_wrapper_srcs in [ + ( + ["site-packages/foo.py"], + [], + [], + ["site-packages/foo.py"] + select({ + Label("//python/config_settings:_is_venvs_site_packages_yes"): [], + "//conditions:default": [], + }), + ["srcs"], + ), + ( + [], + ["site-packages/ext/mod.so"], + ["site-packages/ext/__init__.py"], + [] + select({ + Label("//python/config_settings:_is_venvs_site_packages_yes"): [], + "//conditions:default": ["site-packages/ext/__init__.py"], + }), + select({ + Label("//python/config_settings:_is_venvs_site_packages_yes"): [], + "//conditions:default": ["srcs"], + }), + ), + ]: + py_library_calls = _make_whl_library_targets_py_library_calls( + srcs = srcs, + data = data, + generated_inits = generated_inits, + ) + + env.expect.that_collection(py_library_calls).has_size(2) + if len(py_library_calls) != 2: + return + + env.expect.that_dict(py_library_calls[0]).contains_at_least({ + "srcs": expected_leaf_srcs, + }) + env.expect.that_dict(py_library_calls[1]).contains_exactly({ + "name": "pkg", + "srcs": expected_wrapper_srcs, + "deps": ["srcs", "@pypi//bar:pkg"], + "tags": [], + "visibility": ["//visibility:public"], + }) # buildifier: @unsorted-dict-items + +_tests.append(_test_whl_library_targets_sourceful) + def _test_whl_library_deps_targets_no_deps(env): alias_calls = [] filegroup_calls = []