diff --git a/docs/uv-patching.md b/docs/uv-patching.md index 0f3fb4dd6..6cee8593f 100644 --- a/docs/uv-patching.md +++ b/docs/uv-patching.md @@ -126,6 +126,40 @@ package that resolves to a prebuilt wheel (no source build) fails the build rather than silently dropping the reservation — force a source build with `[tool.uv] no-binary-package` if you need the reservation to apply. +### Monitoring wheel build memory + +Set `monitor_memory` to report the memory observed while building a wheel from +an sdist: + +```starlark +uv.override_package( + lock = "//:uv.lock", + name = "native-package", + monitor_memory = True, +) +``` + +On Linux, rules_py reports the first sample, each 256 MiB high-water crossing, +and the final peak. Reports are flushed as the build runs, so an earlier +high-water mark can remain in the action log when an OOM kills the build. + +The measurement is a best-effort sum of `/proc` RSS for the build process and +its descendants. It can double-count shared pages and miss short-lived +processes. On other platforms it is reported as unavailable. + +`monitor_memory` is diagnostic only. It neither limits memory nor reserves +scheduler capacity, and can be enabled independently from `resource_set`. + +Monitoring runs only when the source-build target is selected. A package with +both an sdist and a compatible wheel produces no report when the wheel is +selected; use `[tool.uv] no-binary-package` to force the monitored source build. +A package with no sdist rejects the override. + +A custom sdist configure tool that returns complete `build_file_content` owns +the wheel action itself. Such content must add its own monitoring; combining +that replacement with `monitor_memory` is rejected rather than silently +dropping the diagnostic. + ### Full replacement To replace a package entirely with a custom target (existing functionality): diff --git a/e2e/BUILD.bazel b/e2e/BUILD.bazel index 13ed6c471..f14d6d0fa 100644 --- a/e2e/BUILD.bazel +++ b/e2e/BUILD.bazel @@ -128,6 +128,14 @@ write_source_files( # rule expands env keys correctly given an explicit fixture. "snapshots/sdist_build.uv_sdist_jdk_build.jpype1.BUILD.bazel": "@sdist_build__uv_sdist_jdk_build__jpype1__1_7_1//:BUILD.bazel", + # @sdist_build for cowsay with uv.override_package(monitor_memory) + # ---------------------------------------------------------------- + # Covers the override_package -> repo-rule -> BUILD-template plumbing + # for a monitor-only source build. The runtime + # //cases/uv-sdist-fallback:test proves that the generated build tool + # can load the conditional monitor dependency and complete the wheel. + "snapshots/sdist_build.uv_sdist_fallback.cowsay.BUILD.bazel": "@sdist_build__uv_sdist_fallback__cowsay__6_0//:BUILD.bazel", + # @sdist_build for python-geohash with uv.override_package(resource_set) # ---------------------------------------------------------------------- # Covers the override_package -> repo-rule -> BUILD-template plumbing diff --git a/e2e/cases/uv-sdist-fallback/setup.MODULE.bazel b/e2e/cases/uv-sdist-fallback/setup.MODULE.bazel index 4ef18bf47..2dc3cb79c 100644 --- a/e2e/cases/uv-sdist-fallback/setup.MODULE.bazel +++ b/e2e/cases/uv-sdist-fallback/setup.MODULE.bazel @@ -16,4 +16,13 @@ uv.project( lock = "//cases/uv-sdist-fallback:uv.lock", pyproject = "//cases/uv-sdist-fallback:pyproject.toml", ) -use_repo(uv, "pypi_sdist_fallback") +uv.override_package( + name = "cowsay", + lock = "//cases/uv-sdist-fallback:uv.lock", + monitor_memory = True, +) +use_repo( + uv, + "pypi_sdist_fallback", + "sdist_build__uv_sdist_fallback__cowsay__6_0", +) diff --git a/e2e/snapshots/sdist_build.uv_sdist_fallback.cowsay.BUILD.bazel b/e2e/snapshots/sdist_build.uv_sdist_fallback.cowsay.BUILD.bazel new file mode 100644 index 000000000..a56f9c271 --- /dev/null +++ b/e2e/snapshots/sdist_build.uv_sdist_fallback.cowsay.BUILD.bazel @@ -0,0 +1,24 @@ + +load("@aspect_rules_py//uv/private/pep517_whl:rule.bzl", "pep517_whl") +load("@aspect_rules_py//py:defs.bzl", "py_binary") + +py_binary( + name = "build_tool", + main = "@aspect_rules_py//uv/private/pep517_whl:build_helper.py", + srcs = ["@aspect_rules_py//uv/private/pep517_whl:build_helper.py"], + deps = ["@aspect_rules_py//uv/private/pep517_whl:memory_monitor", "@@aspect_rules_py++uv+project__uv_sdist_fallback//:build", "@@aspect_rules_py++uv+project__uv_sdist_fallback//:setuptools", "@whl_install__uv_sdist_fallback__setuptools__80_10_2//:install"], +) + +pep517_whl( + name = "whl", + src = "@@aspect_rules_py++uv+sdist__cowsay__47445cb273684618//file:file", + tool = ":build_tool", + version = "6.0", + monitor_memory = True, + visibility = ["//visibility:public"], +) + +exports_files( + ["BUILD.bazel"], + visibility = ["//visibility:public"], +) diff --git a/uv/private/extension/defs.bzl b/uv/private/extension/defs.bzl index 8b53f3269..df2248290 100644 --- a/uv/private/extension/defs.bzl +++ b/uv/private/extension/defs.bzl @@ -254,6 +254,7 @@ def _parse_projects(module_ctx, hub_specs): override.extra_data or override.toolchains or override.env or + override.monitor_memory or override.resource_set != "default" ) @@ -261,7 +262,7 @@ def _parse_projects(module_ctx, hub_specs): fail("uv.override_package() for '{}': `target` is mutually exclusive with modification attributes. Use `target` for full replacement OR build, patch, and data attributes for modifications, not both.".format(override.name)) if not has_target and not has_modifications: - fail("uv.override_package() for '{}': must specify either `target` for full replacement or at least one modification attribute (pre_build_patches, post_install_patches, extra_deps, extra_data, toolchains, env, resource_set).".format(override.name)) + fail("uv.override_package() for '{}': must specify either `target` for full replacement or at least one modification attribute (pre_build_patches, post_install_patches, extra_deps, extra_data, toolchains, env, monitor_memory, resource_set).".format(override.name)) package_overrides[override_key] = override @@ -412,10 +413,12 @@ def _parse_projects(module_ctx, hub_specs): # they don't replace them. Empty == no augmentation. extra_toolchains = [] extra_env = {} + monitor_memory = False resource_set = "default" if pkg_override: extra_toolchains = [str(t) for t in pkg_override.toolchains] extra_env = pkg_override.env + monitor_memory = pkg_override.monitor_memory resource_set = pkg_override.resource_set sbuild_specs[sbuild_id] = struct( @@ -429,6 +432,7 @@ def _parse_projects(module_ctx, hub_specs): configure_command = project.unstable_configure_command, extra_toolchains = extra_toolchains, extra_env = extra_env, + monitor_memory = monitor_memory, resource_set = resource_set, ) @@ -448,6 +452,8 @@ def _parse_projects(module_ctx, hub_specs): if pkg_override and pkg_override.resource_set != "default" and not has_sbuild: fail("uv.override_package() for '{}': `resource_set` reserves resources for the sdist wheel-build action, but this package resolves to a prebuilt wheel (there is no sdist build to reserve for). Remove `resource_set`, or force a source build via `[tool.uv] no-binary-package`.".format(pkg_override.name)) + if pkg_override and pkg_override.monitor_memory and not has_sbuild: + fail("uv.override_package() for '{}': `monitor_memory` observes the sdist wheel-build action, but this package resolves to a prebuilt wheel. Remove `monitor_memory`, or force a source build via `[tool.uv] no-binary-package`.".format(pkg_override.name)) # uv can emit multiple lock records for the same package/version # (e.g. resolution-marker forks), each carrying a different @@ -657,6 +663,8 @@ def _uv_impl(module_ctx): sbuild_kwargs["extra_toolchains"] = sbuild_cfg.extra_toolchains if sbuild_cfg.extra_env: sbuild_kwargs["extra_env"] = sbuild_cfg.extra_env + if sbuild_cfg.monitor_memory: + sbuild_kwargs["monitor_memory"] = True if sbuild_cfg.resource_set != "default": sbuild_kwargs["resource_set"] = sbuild_cfg.resource_set sdist_build(**sbuild_kwargs) @@ -755,6 +763,10 @@ _override_package_tag = tag_class( # Full replacement: provide a target that substitutes for the package entirely. # Mutually exclusive with patch/exclude attributes. "target": attr.label(mandatory = False), + "monitor_memory": attr.bool( + default = False, + doc = "Report approximate Linux process-tree RSS while building this package's wheel.", + ), # Per-package local execution resources for the wheel build action. # Uses bazel-lib's predefined resource_set vocabulary (the same enum diff --git a/uv/private/pep517_whl/BUILD.bazel b/uv/private/pep517_whl/BUILD.bazel index 970f84f83..196da680e 100644 --- a/uv/private/pep517_whl/BUILD.bazel +++ b/uv/private/pep517_whl/BUILD.bazel @@ -4,7 +4,7 @@ load("@bazel_skylib//rules:write_file.bzl", "write_file") load("@rules_shell//shell:sh_binary.bzl", "sh_binary") load("@tar.bzl//tar:mtree.bzl", "mtree_mutate", "mtree_spec") load("@tar.bzl//tar:tar.bzl", "tar_rule") -load("//py:defs.bzl", "py_binary") +load("//py:defs.bzl", "py_binary", "py_library", "py_test") load(":rule.bzl", "pep517_native_whl", "pep517_whl") load(":test.bzl", "hostile_python_env_target", "pep517_native_whl_toolchain_env_test") @@ -26,6 +26,19 @@ bzl_library( ], ) +py_library( + name = "memory_monitor", + srcs = ["memory_monitor.py"], + visibility = ["//visibility:public"], +) + +py_test( + name = "memory_monitor_test", + srcs = ["memory_monitor_test.py"], + main = "memory_monitor_test.py", + deps = [":memory_monitor"], +) + py_binary( name = "__build_helper", testonly = True, diff --git a/uv/private/pep517_whl/build_helper.py b/uv/private/pep517_whl/build_helper.py index 10774c6c3..c3d5a5ba3 100644 --- a/uv/private/pep517_whl/build_helper.py +++ b/uv/private/pep517_whl/build_helper.py @@ -187,6 +187,7 @@ def _legacy_metadata_conflicts_with_pyproject(worktree): PARSER = ArgumentParser() PARSER.add_argument("srcarchive") PARSER.add_argument("outdir") +PARSER.add_argument("--monitor-memory", action="store_true") PARSER.add_argument("--validate-anyarch", action="store_true") PARSER.add_argument("--patch-strip", type=int, default=0, help="Strip count for patch (-p)") PARSER.add_argument("--patch", action="append", default=[], dest="patches", help="Patch file to apply (repeatable)") @@ -279,7 +280,20 @@ def _legacy_metadata_conflicts_with_pyproject(worktree): with TemporaryFile(mode="w+") as build_log: try: - run(cmd, cwd=t, env=build_env, stdout=build_log, stderr=STDOUT, check=True) + if opts.monitor_memory: + # Generated build tools include this dependency only when the + # corresponding wheel opts into monitoring. + from uv.private.pep517_whl.memory_monitor import run_with_memory_monitor + + run_with_memory_monitor( + cmd, + cwd=t, + env=build_env, + stdout=build_log, + wheel=path.basename(opts.srcarchive), + ) + else: + run(cmd, cwd=t, env=build_env, stdout=build_log, stderr=STDOUT, check=True) except CalledProcessError: build_log.seek(0) output = build_log.read() diff --git a/uv/private/pep517_whl/memory_monitor.py b/uv/private/pep517_whl/memory_monitor.py new file mode 100644 index 000000000..2bc558cc8 --- /dev/null +++ b/uv/private/pep517_whl/memory_monitor.py @@ -0,0 +1,116 @@ +"""Report approximate Linux RSS while a wheel build runs.""" + +import os +import subprocess +import sys +import time + +_MIB = 1024 * 1024 +_REPORT_STEP_BYTES = 256 * _MIB +_SAMPLE_INTERVAL_SECONDS = 0.25 + + +def _process_tree_rss_bytes(root_pid, page_size, proc_root="/proc"): + """Return the sampled RSS sum for root_pid and its descendants.""" + # statm reports resident memory in pages: + # https://man7.org/linux/man-pages/man5/proc_pid_statm.5.html + # Each task's children file lists that task's immediate children: + # https://docs.kernel.org/filesystems/proc.html#proc-pid-task-tid-children-information-about-task-children + pending = [root_pid] + seen = set() + total = 0 + sampled = False + + while pending: + pid = pending.pop() + if pid in seen: + continue + seen.add(pid) + process_dir = os.path.join(proc_root, str(pid)) + + try: + with open(os.path.join(process_dir, "statm")) as statm: + total += int(statm.read().split()[1]) * page_size + sampled = True + except (FileNotFoundError, PermissionError, ProcessLookupError, ValueError, IndexError): + pass + + try: + task_ids = os.listdir(os.path.join(process_dir, "task")) + except (FileNotFoundError, PermissionError, ProcessLookupError): + continue + + for task_id in task_ids: + try: + with open(os.path.join(process_dir, "task", task_id, "children")) as children: + pending.extend(int(child) for child in children.read().split()) + except (FileNotFoundError, PermissionError, ProcessLookupError, ValueError): + continue + + return total if sampled else None + + +def _report_memory(wheel, state, peak, current=None, returncode=None): + if peak is None: + details = "unavailable" + elif current is None: + details = "peak={:.1f} MiB".format(peak / _MIB) + else: + details = "current={:.1f} MiB, peak={:.1f} MiB".format( + current / _MIB, + peak / _MIB, + ) + if returncode is not None: + details += ", return code={}".format(returncode) + if returncode == -9: + details += " (SIGKILL; possible OOM)" + print( + "rules_py wheel build memory for {} {}: {} (approximate aggregate RSS)".format( + wheel, + state, + details, + ), + file=sys.stderr, + flush=True, + ) + + +def run_with_memory_monitor(cmd, *, cwd, env, stdout, wheel): + """Run cmd while reporting best-effort process-tree RSS.""" + try: + can_sample = sys.platform.startswith("linux") and os.path.isdir("/proc") + page_size = os.sysconf("SC_PAGE_SIZE") if can_sample else None + except (OSError, ValueError): + can_sample = False + page_size = None + + process = subprocess.Popen( + cmd, + cwd=cwd, + env=env, + stdout=stdout, + stderr=subprocess.STDOUT, + ) + peak = None + next_report = 0 + + while True: + if can_sample: + try: + current = _process_tree_rss_bytes(process.pid, page_size) + except (OSError, ValueError): + current = None + if current is not None: + peak = current if peak is None else max(peak, current) + if peak >= next_report: + _report_memory(wheel, "running", peak, current=current) + next_report = (peak // _REPORT_STEP_BYTES + 1) * _REPORT_STEP_BYTES + + returncode = process.poll() + if returncode is not None: + break + time.sleep(_SAMPLE_INTERVAL_SECONDS) + + _report_memory(wheel, "finished", peak, returncode=returncode) + if returncode: + raise subprocess.CalledProcessError(returncode, cmd) diff --git a/uv/private/pep517_whl/memory_monitor_test.py b/uv/private/pep517_whl/memory_monitor_test.py new file mode 100644 index 000000000..0e4a933c9 --- /dev/null +++ b/uv/private/pep517_whl/memory_monitor_test.py @@ -0,0 +1,158 @@ +import contextlib +import io +import itertools +import os +from pathlib import Path +import signal +import subprocess +import sys +import tempfile +import unittest +from unittest import mock + +from uv.private.pep517_whl import memory_monitor + + +class MemoryMonitorTest(unittest.TestCase): + def test_sums_descendants_spawned_by_any_task(self): + with tempfile.TemporaryDirectory() as directory: + proc_root = Path(directory) + self._write_process(proc_root, 10, 2, {10: [20], 11: [30]}) + self._write_process(proc_root, 20, 3, {20: []}) + self._write_process(proc_root, 30, 5, {30: []}) + + self.assertEqual( + 10 * 4096, + memory_monitor._process_tree_rss_bytes(10, 4096, str(proc_root)), + ) + + def test_walks_children_when_parent_rss_disappears(self): + with tempfile.TemporaryDirectory() as directory: + proc_root = Path(directory) + self._write_process(proc_root, 10, None, {10: [20]}) + self._write_process(proc_root, 20, 3, {20: []}) + + self.assertEqual( + 3 * 4096, + memory_monitor._process_tree_rss_bytes(10, 4096, str(proc_root)), + ) + + def test_preserves_peak_after_sampling_failure(self): + stderr = io.StringIO() + with ( + tempfile.TemporaryFile() as stdout, + contextlib.redirect_stderr(stderr), + mock.patch.object(memory_monitor.sys, "platform", "linux"), + mock.patch.object(memory_monitor.os.path, "isdir", return_value=True), + mock.patch.object(memory_monitor, "_SAMPLE_INTERVAL_SECONDS", 0.01), + mock.patch.object( + memory_monitor, + "_process_tree_rss_bytes", + side_effect=itertools.chain( + [16 * memory_monitor._MIB], + itertools.repeat(OSError("procfs race")), + ), + ), + ): + memory_monitor.run_with_memory_monitor( + [sys.executable, "-c", "import time; time.sleep(.05)"], + cwd=None, + env=os.environ.copy(), + stdout=stdout, + wheel="test-wheel.tar.gz", + ) + + self.assertIn("peak=16.0 MiB", stderr.getvalue()) + + @unittest.skipUnless( + sys.platform.startswith("linux") and os.path.isdir("/proc"), + "requires Linux procfs", + ) + def test_failure_reports_peak_and_raises_child_returncode(self): + stderr = io.StringIO() + with ( + tempfile.TemporaryFile() as stdout, + contextlib.redirect_stderr(stderr), + mock.patch.object(memory_monitor, "_REPORT_STEP_BYTES", 1), + ): + with self.assertRaises(subprocess.CalledProcessError) as raised: + memory_monitor.run_with_memory_monitor( + [ + sys.executable, + "-c", + "import time; " + "data = bytearray(16 * 1024 * 1024); " + "time.sleep(.5); " + "raise SystemExit(7)", + ], + cwd=None, + env=os.environ.copy(), + stdout=stdout, + wheel="test-wheel.tar.gz", + ) + + self.assertEqual(7, raised.exception.returncode) + self.assertRegex(stderr.getvalue(), r"peak=[1-9][0-9.]* MiB, return code=7") + + @unittest.skipUnless(hasattr(signal, "SIGKILL"), "requires SIGKILL") + def test_sigkill_is_reported_as_a_possible_oom(self): + stderr = io.StringIO() + with ( + tempfile.TemporaryFile() as stdout, + contextlib.redirect_stderr(stderr), + ): + with self.assertRaises(subprocess.CalledProcessError) as raised: + memory_monitor.run_with_memory_monitor( + [ + sys.executable, + "-c", + "import os, signal, time; " + "data = bytearray(16 * 1024 * 1024); " + "time.sleep(.5); " + "os.kill(os.getpid(), signal.SIGKILL)", + ], + cwd=None, + env=os.environ.copy(), + stdout=stdout, + wheel="test-wheel.tar.gz", + ) + + self.assertEqual(-signal.SIGKILL, raised.exception.returncode) + self.assertIn("SIGKILL; possible OOM", stderr.getvalue()) + + @unittest.skipUnless(hasattr(os, "getpgrp"), "requires POSIX process groups") + def test_child_stays_in_callers_process_group(self): + with ( + tempfile.TemporaryFile(mode="w+") as stdout, + contextlib.redirect_stderr(io.StringIO()), + ): + memory_monitor.run_with_memory_monitor( + [sys.executable, "-c", "import os; print(os.getpgrp())"], + cwd=None, + env=os.environ.copy(), + stdout=stdout, + wheel="test-wheel.tar.gz", + ) + stdout.seek(0) + child_process_group = int(stdout.read()) + + self.assertEqual(os.getpgrp(), child_process_group) + + @staticmethod + def _write_process(proc_root, pid, rss_pages, task_children): + process_dir = proc_root / str(pid) + process_dir.mkdir() + if rss_pages is not None: + (process_dir / "statm").write_text( + "100 {} 0 0 0 0 0\n".format(rss_pages) + ) + for task_id, children in task_children.items(): + task_dir = process_dir / "task" / str(task_id) + task_dir.mkdir(parents=True) + (task_dir / "children").write_text( + " ".join(str(child) for child in children) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/uv/private/pep517_whl/rule.bzl b/uv/private/pep517_whl/rule.bzl index fab4647f2..ee0e25b57 100644 --- a/uv/private/pep517_whl/rule.bzl +++ b/uv/private/pep517_whl/rule.bzl @@ -46,6 +46,9 @@ def _patch_args_and_inputs(ctx): patch_inputs.append(f) return patch_args, patch_inputs +def _memory_args(ctx): + return ["--monitor-memory"] if ctx.attr.monitor_memory else [] + def _collect_toolchain_inputs_and_vars(ctx): """Gather files + Make-variable substitutions from `ctx.attr.toolchains`. @@ -127,7 +130,7 @@ def _pep517_whl(ctx): progress_message = "Source compiling {} to a whl".format(archive.basename), executable = ctx.executable.tool, toolchain = None, - arguments = ctx.attr.args + patch_args + [ + arguments = ctx.attr.args + patch_args + _memory_args(ctx) + [ archive.path, wheel_dir.path, ], @@ -165,7 +168,7 @@ def _pep517_native_whl(ctx): progress_message = "Native source compiling {} to a whl".format(archive.basename), executable = ctx.executable.tool, toolchain = None, - arguments = ctx.attr.args + patch_args + [ + arguments = ctx.attr.args + patch_args + _memory_args(ctx) + [ archive.path, wheel_dir.path, ], @@ -199,6 +202,10 @@ _pep517_whl_attrs = { "tool": attr.label(executable = True, cfg = "exec"), "version": attr.string(), "args": attr.string_list(default = ["--validate-anyarch"]), + "monitor_memory": attr.bool( + default = False, + doc = "Report approximate Linux process-tree RSS while building the wheel.", + ), } | _PATCH_ATTRS | resource_set_attr pep517_whl = rule( diff --git a/uv/private/sdist_build/repository.bzl b/uv/private/sdist_build/repository.bzl index 38558b5c5..a61ed478c 100644 --- a/uv/private/sdist_build/repository.bzl +++ b/uv/private/sdist_build/repository.bzl @@ -172,6 +172,8 @@ def _sdist_build_impl(repository_ctx): # If the tool provided complete build file content, use it directly. build_file_content = inspection.get("build_file_content") if build_file_content: + if repository_ctx.attr.monitor_memory: + fail("sdist_build for '{}': the configure tool returned complete `build_file_content`, which bypasses the generated `pep517_*whl(...)` call, so `monitor_memory` cannot be applied. Drop `monitor_memory` from the override, or add equivalent monitoring to the configure tool's `build_file_content`.".format(repository_ctx.name)) if repository_ctx.attr.resource_set != "default": fail("sdist_build for '{}': the configure tool returned complete `build_file_content`, which bypasses the generated `pep517_*whl(...)` call, so `resource_set = \"{}\"` cannot be applied. Drop `resource_set` from the override, or have the configure tool set `resource_set` in its own `build_file_content`.".format( repository_ctx.name, @@ -209,6 +211,13 @@ def _sdist_build_impl(repository_ctx): # Merge explicit deps with auto-discovered deps all_deps = [str(d) for d in repository_ctx.attr.deps] + extra_dep_labels + monitor_memory_attr = "" + if repository_ctx.attr.monitor_memory: + all_deps = [ + "@aspect_rules_py//uv/private/pep517_whl:memory_monitor", + ] + all_deps + monitor_memory_attr = "\n monitor_memory = True," + pre_build_patches = repository_ctx.attr.pre_build_patches patch_attrs = "" if pre_build_patches: @@ -276,7 +285,7 @@ py_binary( name = "whl", src = "{src}", tool = ":build_tool", - version = "{version}",{resource_set_attr}{patch_attrs}{toolchain_attrs} + version = "{version}",{monitor_memory_attr}{resource_set_attr}{patch_attrs}{toolchain_attrs} visibility = ["//visibility:public"], ) @@ -287,6 +296,7 @@ exports_files( """.format( src = repository_ctx.attr.src, deps = repr(all_deps), + monitor_memory_attr = monitor_memory_attr, rule = "pep517_native_whl" if is_native else "pep517_whl", version = repository_ctx.attr.version, resource_set_attr = resource_set_attr, @@ -314,6 +324,10 @@ sdist_build = repository_rule( "two arguments. See //uv/private/sdist_configure:defs.bzl.", ), "version": attr.string(), + "monitor_memory": attr.bool( + default = False, + doc = "Whether to report approximate Linux process-tree RSS for the wheel build.", + ), "resource_set": attr.string( default = "default", doc = "bazel-lib resource_set name forwarded to the generated pep517_*whl(...) " +