Skip to content

Commit eda9fe3

Browse files
stevebarrauaignas
andauthored
feat: add package metadata for wheel libraries (bazel-contrib#3531)
Add package_metadata rule to generated BUILD files for wheel libraries to track package provenance using PURL (Package URL) format. This is then picked up by [supply_chain_tools](https://registry.bazel.build/modules/supply_chain_tools) to produce SBOM for python target using external dependencies. Fixes bazel-contrib#2054 --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com>
1 parent 78ae1f0 commit eda9fe3

7 files changed

Lines changed: 83 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ END_UNRELEASED_TEMPLATE
8888
* (toolchains) `3.13.12`, `3.14.3` Python toolchain from [20260325] release.
8989
* (toolchains) `3.10.20`, `3.11.15`, `3.12.13`, `3.13.13` `3.14.4`, `3.15.0a8`
9090
* Python toolchain from [20260414] release.
91+
* (pypi) `package_metadata` support, fixes
92+
[#2054](https://github.com/bazel-contrib/rules_python/issues/2054).
9193

9294
[20260325]: https://github.com/astral-sh/python-build-standalone/releases/tag/20260325
9395
[20260414]: https://github.com/astral-sh/python-build-standalone/releases/tag/20260414

MODULE.bazel

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ module(
66

77
bazel_dep(name = "bazel_features", version = "1.21.0")
88
bazel_dep(name = "bazel_skylib", version = "1.8.2")
9-
bazel_dep(name = "rules_cc", version = "0.1.5")
9+
bazel_dep(name = "package_metadata", version = "0.0.7")
1010
bazel_dep(name = "platforms", version = "0.0.11")
11+
bazel_dep(name = "rules_cc", version = "0.1.5")
1112

1213
# Those are loaded only when using py_proto_library
1314
# Use py_proto_library directly from protobuf repository

python/private/py_repositories.bzl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ def py_repositories(transition_settings = []):
7272
strip_prefix = "rules_cc-0.1.5",
7373
urls = ["https://github.com/bazelbuild/rules_cc/releases/download/0.1.5/rules_cc-0.1.5.tar.gz"],
7474
)
75+
http_archive(
76+
name = "package_metadata",
77+
sha256 = "8f27dc7393e3f3bdc793bdc4ba36d67a63c22cc9d38cc65d3204654974ea4563",
78+
strip_prefix = "supply-chain-0.0.7/metadata",
79+
url = "https://github.com/bazel-contrib/supply-chain/releases/download/v0.0.7/supply-chain-v0.0.7.tar.gz",
80+
)
7581

7682
# Needed by rules_cc, triggered by @rules_java_prebuilt in Bazel by using @rules_cc//cc:defs.bzl
7783
# NOTE: This name must be com_google_protobuf until Bazel drops WORKSPACE

python/private/pypi/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ bzl_library(
495495
"//python/private:auth_bzl",
496496
"//python/private:envsubst_bzl",
497497
"//python/private:is_standalone_interpreter_bzl",
498+
"//python/private:normalize_name_bzl",
498499
"//python/private:repo_utils_bzl",
499500
"//python/private:util_bzl",
500501
"@rules_python_internal//:rules_python_config_bzl",

python/private/pypi/generate_whl_library_build_bazel.bzl

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ _TEMPLATE = """\
4040
4141
package(default_visibility = ["//visibility:public"])
4242
43+
package_metadata(
44+
name = "package_metadata",
45+
purl = {purl},
46+
visibility = ["//:__subpackages__"],
47+
)
48+
4349
{fn}(
4450
{kwargs}
4551
)
@@ -49,20 +55,25 @@ def generate_whl_library_build_bazel(
4955
*,
5056
annotation = None,
5157
default_python_version = None,
58+
purl = None,
5259
**kwargs):
5360
"""Generate a BUILD file for an unzipped Wheel
5461
5562
Args:
5663
annotation: The annotation for the build file.
5764
default_python_version: The python version to use to parse the METADATA.
65+
purl: The purl.
5866
**kwargs: Extra args serialized to be passed to the
5967
{obj}`whl_library_targets`.
6068
6169
Returns:
6270
A complete BUILD file as a string
6371
"""
6472

65-
loads = []
73+
loads = [
74+
"""load("@package_metadata//rules:package_metadata.bzl", "package_metadata")""",
75+
]
76+
6677
if kwargs.get("tags"):
6778
fn = "whl_library_targets"
6879

@@ -119,6 +130,7 @@ def generate_whl_library_build_bazel(
119130
"{} = {},".format(k, _RENDER.get(k, repr)(v))
120131
for k, v in sorted(kwargs.items())
121132
])),
133+
purl = repr(purl),
122134
),
123135
] + additional_content,
124136
)

python/private/pypi/whl_library.bzl

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config")
1818
load("//python/private:auth.bzl", "AUTH_ATTRS", "get_auth")
1919
load("//python/private:envsubst.bzl", "envsubst")
2020
load("//python/private:is_standalone_interpreter.bzl", "is_standalone_interpreter")
21+
load("//python/private:normalize_name.bzl", "normalize_name")
2122
load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils")
2223
load(":attrs.bzl", "ATTRS", "use_isolated")
2324
load(":deps.bzl", "all_repo_names", "record_files")
@@ -276,6 +277,24 @@ def _extract_whl_py(rctx, *, python_interpreter, args, whl_path, environment, lo
276277
logger = logger,
277278
)
278279

280+
def _to_purl(*, index, metadata, filename):
281+
"""
282+
Produce a PyPI PURL from the metadata.
283+
284+
https://github.com/package-url/purl-spec/blob/main/types-doc/pypi-definition.md
285+
"""
286+
287+
# https://github.com/package-url/purl-spec/blob/main/types-doc/pypi-definition.md#name-definition
288+
name = normalize_name(metadata.name).replace("_", "-")
289+
290+
qualifiers = {}
291+
if index:
292+
qualifiers["repository_url"] = index
293+
if filename:
294+
qualifiers["file_name"] = filename
295+
296+
return "pkg:pypi/{}@{}?{}".format(name, metadata.version, "&".join(["{}={}".format(key, val) for key, val in qualifiers.items()]))
297+
279298
def _whl_library_impl(rctx):
280299
logger = repo_utils.logger(rctx)
281300

@@ -436,14 +455,25 @@ def _whl_library_impl(rctx):
436455
group_name = rctx.attr.group_name,
437456
namespace_package_files = namespace_package_files,
438457
extras = requirement(rctx.attr.requirement).extras,
458+
purl = _to_purl(
459+
index = rctx.attr.index_url,
460+
metadata = metadata,
461+
filename = sdist_filename or whl_path.basename,
462+
),
439463
)
440464

441465
# Delete these in case the wheel had them. They generally don't cause
442466
# a problem, but let's avoid the chance of that happening.
443467
rctx.file("WORKSPACE")
444468
rctx.file("WORKSPACE.bazel")
445469
rctx.file("MODULE.bazel")
446-
rctx.file("REPO.bazel")
470+
rctx.file("REPO.bazel", """\
471+
repo(
472+
default_package_metadata = [
473+
"//:package_metadata",
474+
],
475+
)
476+
""")
447477

448478
# BUILD files interfere with globbing and Bazel package boundaries.
449479
_remove_files(rctx, "BUILD", "BUILD.bazel")

tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,17 @@ _tests = []
2121

2222
def _test_all_legacy(env):
2323
want = """\
24+
load("@package_metadata//rules:package_metadata.bzl", "package_metadata")
2425
load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets")
2526
2627
package(default_visibility = ["//visibility:public"])
2728
29+
package_metadata(
30+
name = "package_metadata",
31+
purl = None,
32+
visibility = ["//:__subpackages__"],
33+
)
34+
2835
whl_library_targets(
2936
copy_executables = {
3037
"exec_src": "exec_dest",
@@ -79,11 +86,18 @@ _tests.append(_test_all_legacy)
7986

8087
def _test_all_workspace(env):
8188
want = """\
89+
load("@package_metadata//rules:package_metadata.bzl", "package_metadata")
8290
load("@pypi//:config.bzl", "packages")
8391
load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires")
8492
8593
package(default_visibility = ["//visibility:public"])
8694
95+
package_metadata(
96+
name = "package_metadata",
97+
purl = None,
98+
visibility = ["//:__subpackages__"],
99+
)
100+
87101
whl_library_targets_from_requires(
88102
copy_executables = {
89103
"exec_src": "exec_dest",
@@ -138,11 +152,18 @@ _tests.append(_test_all_workspace)
138152

139153
def _test_all(env):
140154
want = """\
155+
load("@package_metadata//rules:package_metadata.bzl", "package_metadata")
141156
load("@pypi//:config.bzl", "packages")
142157
load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires")
143158
144159
package(default_visibility = ["//visibility:public"])
145160
161+
package_metadata(
162+
name = "package_metadata",
163+
purl = None,
164+
visibility = ["//:__subpackages__"],
165+
)
166+
146167
whl_library_targets_from_requires(
147168
copy_executables = {
148169
"exec_src": "exec_dest",
@@ -197,11 +218,18 @@ _tests.append(_test_all)
197218

198219
def _test_all_with_loads(env):
199220
want = """\
221+
load("@package_metadata//rules:package_metadata.bzl", "package_metadata")
200222
load("@pypi//:config.bzl", "packages")
201223
load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires")
202224
203225
package(default_visibility = ["//visibility:public"])
204226
227+
package_metadata(
228+
name = "package_metadata",
229+
purl = None,
230+
visibility = ["//:__subpackages__"],
231+
)
232+
205233
whl_library_targets_from_requires(
206234
copy_executables = {
207235
"exec_src": "exec_dest",

0 commit comments

Comments
 (0)