fix: ignore_root_user_error should be taken from root module only (#1662)
Only the root module should be able to decide `ignore_root_user_error`.
Modules being depended upon don't know the final environment, so they
aren't in the right position to know or decide what the correct setting
is. This PR implements a solution to take the `ignore_root_user_error`
value from the root module only.
Fixes #1658
---------
Co-authored-by: Richard Levasseur <rlevasseur@google.com>
diff --git a/.bazelrc b/.bazelrc
index 250377a..9ecb892 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -4,8 +4,8 @@
# (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it)
# To update these lines, execute
# `bazel run @rules_bazel_integration_test//tools:update_deleted_packages`
-build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/dupe_requirements,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/ignore_root_user_error,tests/integration/pip_repository_entry_points,tests/integration/py_cc_toolchain_registered
-query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/dupe_requirements,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/ignore_root_user_error,tests/integration/pip_repository_entry_points,tests/integration/py_cc_toolchain_registered
+build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/dupe_requirements,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/ignore_root_user_error,tests/integration/pip_repository_entry_points,tests/integration/py_cc_toolchain_registered,tests/integration/ignore_root_user_error/submodule
+query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/dupe_requirements,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/ignore_root_user_error,tests/integration/pip_repository_entry_points,tests/integration/py_cc_toolchain_registered,tests/integration/ignore_root_user_error/submodule
test --test_output=errors
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b861902..07b68ec 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -124,6 +124,9 @@
* (toolchains) Workspace builds register the py cc toolchain (bzlmod already
was). This makes e.g. `//python/cc:current_py_cc_headers` Just Work.
([#1669](https://github.com/bazelbuild/rules_python/issues/1669))
+* (bzlmod python.toolchain) The value of `ignore_root_user_error` is now decided
+ by the root module only.
+ ([#1658](https://github.com/bazelbuild/rules_python/issues/1658))
### Added
diff --git a/python/extensions/BUILD.bazel b/python/extensions/BUILD.bazel
index 88e3984..a9dede4 100644
--- a/python/extensions/BUILD.bazel
+++ b/python/extensions/BUILD.bazel
@@ -35,5 +35,8 @@
name = "python_bzl",
srcs = ["python.bzl"],
visibility = ["//:__subpackages__"],
- deps = ["//python/private/bzlmod:python_bzl"],
+ deps = [
+ "//python/private:util_bzl",
+ "//python/private/bzlmod:python_bzl",
+ ],
)
diff --git a/python/private/bzlmod/python.bzl b/python/private/bzlmod/python.bzl
index 3b59d5b..5862f00 100644
--- a/python/private/bzlmod/python.bzl
+++ b/python/private/bzlmod/python.bzl
@@ -16,6 +16,7 @@
load("//python:repositories.bzl", "python_register_toolchains")
load("//python/private:toolchains_repo.bzl", "multi_toolchain_aliases")
+load("//python/private:util.bzl", "IS_BAZEL_6_4_OR_HIGHER")
load(":pythons_hub.bzl", "hub_repo")
# This limit can be increased essentially arbitrarily, but doing so will cause a rebuild of all
@@ -43,14 +44,14 @@
def _print_warn(msg):
print("WARNING:", msg)
-def _python_register_toolchains(name, toolchain_attr, module):
+def _python_register_toolchains(name, toolchain_attr, module, ignore_root_user_error):
"""Calls python_register_toolchains and returns a struct used to collect the toolchains.
"""
python_register_toolchains(
name = name,
python_version = toolchain_attr.python_version,
register_coverage_tool = toolchain_attr.configure_coverage_tool,
- ignore_root_user_error = toolchain_attr.ignore_root_user_error,
+ ignore_root_user_error = ignore_root_user_error,
)
return struct(
python_version = toolchain_attr.python_version,
@@ -59,6 +60,13 @@
)
def _python_impl(module_ctx):
+ if module_ctx.os.environ.get("RULES_PYTHON_BZLMOD_DEBUG", "0") == "1":
+ debug_info = {
+ "toolchains_registered": [],
+ }
+ else:
+ debug_info = None
+
# The toolchain_info structs to register, in the order to register them in.
# NOTE: The last element is special: it is treated as the default toolchain,
# so there is special handling to ensure the last entry is the correct one.
@@ -72,6 +80,13 @@
# Map of string Major.Minor to the toolchain_info struct
global_toolchain_versions = {}
+ ignore_root_user_error = None
+
+ # if the root module does not register any toolchain then the
+ # ignore_root_user_error takes its default value: False
+ if not module_ctx.modules[0].tags.toolchain:
+ ignore_root_user_error = False
+
for mod in module_ctx.modules:
module_toolchain_versions = []
@@ -84,16 +99,27 @@
_fail_duplicate_module_toolchain_version(toolchain_version, mod.name)
module_toolchain_versions.append(toolchain_version)
- # Only the root module and rules_python are allowed to specify the default
- # toolchain for a couple reasons:
- # * It prevents submodules from specifying different defaults and only
- # one of them winning.
- # * rules_python needs to set a soft default in case the root module doesn't,
- # e.g. if the root module doesn't use Python itself.
- # * The root module is allowed to override the rules_python default.
if mod.is_root:
+ # Only the root module and rules_python are allowed to specify the default
+ # toolchain for a couple reasons:
+ # * It prevents submodules from specifying different defaults and only
+ # one of them winning.
+ # * rules_python needs to set a soft default in case the root module doesn't,
+ # e.g. if the root module doesn't use Python itself.
+ # * The root module is allowed to override the rules_python default.
+
# A single toolchain is treated as the default because it's unambiguous.
is_default = toolchain_attr.is_default or len(mod.tags.toolchain) == 1
+
+ # Also only the root module should be able to decide ignore_root_user_error.
+ # Modules being depended upon don't know the final environment, so they aren't
+ # in the right position to know or decide what the correct setting is.
+
+ # If an inconsistency in the ignore_root_user_error among multiple toolchains is detected, fail.
+ if ignore_root_user_error != None and toolchain_attr.ignore_root_user_error != ignore_root_user_error:
+ fail("Toolchains in the root module must have consistent 'ignore_root_user_error' attributes")
+
+ ignore_root_user_error = toolchain_attr.ignore_root_user_error
elif mod.name == "rules_python" and not default_toolchain:
# We don't do the len() check because we want the default that rules_python
# sets to be clearly visible.
@@ -128,8 +154,14 @@
toolchain_name,
toolchain_attr,
module = mod,
+ ignore_root_user_error = ignore_root_user_error,
)
global_toolchain_versions[toolchain_version] = toolchain_info
+ if debug_info:
+ debug_info["toolchains_registered"].append({
+ "ignore_root_user_error": ignore_root_user_error,
+ "name": toolchain_name,
+ })
if is_default:
# This toolchain is setting the default, but the actual
@@ -192,6 +224,12 @@
},
)
+ if debug_info != None:
+ _debug_repo(
+ name = "rules_python_bzlmod_debug",
+ debug_info = json.encode_indent(debug_info),
+ )
+
def _fail_duplicate_module_toolchain_version(version, module):
fail(("Duplicate module toolchain version: module '{module}' attempted " +
"to use version '{version}' multiple times in itself").format(
@@ -220,6 +258,14 @@
second = second,
))
+def _get_bazel_version_specific_kwargs():
+ kwargs = {}
+
+ if IS_BAZEL_6_4_OR_HIGHER:
+ kwargs["environ"] = ["RULES_PYTHON_BZLMOD_DEBUG"]
+
+ return kwargs
+
python = module_extension(
doc = """Bzlmod extension that is used to register Python toolchains.
""",
@@ -263,7 +309,16 @@
),
"ignore_root_user_error": attr.bool(
default = False,
- doc = "Whether the check for root should be ignored or not. This causes cache misses with .pyc files.",
+ doc = """\
+If False, the Python runtime installation will be made read only. This improves
+the ability for Bazel to cache it, but prevents the interpreter from creating
+pyc files for the standard library dynamically at runtime as they are loaded.
+
+If True, the Python runtime installation is read-write. This allows the
+interpreter to create pyc files for the standard library, but, because they are
+created as needed, it adversely affects Bazel's ability to cache the runtime and
+can result in spurious build failures.
+""",
mandatory = False,
),
"is_default": attr.bool(
@@ -279,4 +334,23 @@
},
),
},
+ **_get_bazel_version_specific_kwargs()
+)
+
+_DEBUG_BUILD_CONTENT = """
+package(
+ default_visibility = ["//visibility:public"],
+)
+exports_files(["debug_info.json"])
+"""
+
+def _debug_repo_impl(repo_ctx):
+ repo_ctx.file("BUILD.bazel", _DEBUG_BUILD_CONTENT)
+ repo_ctx.file("debug_info.json", repo_ctx.attr.debug_info)
+
+_debug_repo = repository_rule(
+ implementation = _debug_repo_impl,
+ attrs = {
+ "debug_info": attr.string(),
+ },
)
diff --git a/python/private/util.bzl b/python/private/util.bzl
index 71476f9..16b8ff8 100644
--- a/python/private/util.bzl
+++ b/python/private/util.bzl
@@ -89,3 +89,16 @@
# Bazel 5.4 has a bug where every access of testing.ExecutionInfo is a
# different object that isn't equal to any other. This is fixed in bazel 6+.
IS_BAZEL_6_OR_HIGHER = testing.ExecutionInfo == testing.ExecutionInfo
+
+_marker_rule_to_detect_bazel_6_4_or_higher = rule(implementation = lambda ctx: None)
+
+# Bazel 6.4 and higher have a bug fix where rule names show up in the str()
+# of a rule. See
+# https://github.com/bazelbuild/bazel/commit/002490b9a2376f0b2ea4a37102c5e94fc50a65ba
+# https://github.com/bazelbuild/bazel/commit/443cbcb641e17f7337ccfdecdfa5e69bc16cae55
+# This technique is done instead of using native.bazel_version because,
+# under stardoc, the native.bazel_version attribute is entirely missing, which
+# prevents doc generation from being able to correctly generate docs.
+IS_BAZEL_6_4_OR_HIGHER = "_marker_rule_to_detect_bazel_6_4_or_higher" in str(
+ _marker_rule_to_detect_bazel_6_4_or_higher,
+)
diff --git a/tests/integration/ignore_root_user_error/.bazelrc b/tests/integration/ignore_root_user_error/.bazelrc
index f23315a..fde94c7 100644
--- a/tests/integration/ignore_root_user_error/.bazelrc
+++ b/tests/integration/ignore_root_user_error/.bazelrc
@@ -1,3 +1,5 @@
+common --action_env=RULES_PYTHON_BZLMOD_DEBUG=1
+common --lockfile_mode=off
test --test_output=errors
# Windows requires these for multi-python support:
diff --git a/tests/integration/ignore_root_user_error/BUILD.bazel b/tests/integration/ignore_root_user_error/BUILD.bazel
index f907624..6e3b7b9 100644
--- a/tests/integration/ignore_root_user_error/BUILD.bazel
+++ b/tests/integration/ignore_root_user_error/BUILD.bazel
@@ -1,7 +1,32 @@
-load("@rules_python//python:defs.bzl", "py_test")
+# Copyright 2024 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.
+
+load("@rules_python//python:py_test.bzl", "py_test")
+load("@rules_python//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility
py_test(
name = "foo_test",
srcs = ["foo_test.py"],
visibility = ["//visibility:public"],
)
+
+py_test(
+ name = "bzlmod_test",
+ srcs = ["bzlmod_test.py"],
+ data = [
+ "@rules_python//python/runfiles",
+ "@rules_python_bzlmod_debug//:debug_info.json",
+ ],
+ target_compatible_with = [] if BZLMOD_ENABLED else ["@platforms//:incompatible"],
+)
diff --git a/tests/integration/ignore_root_user_error/MODULE.bazel b/tests/integration/ignore_root_user_error/MODULE.bazel
new file mode 100644
index 0000000..15c37c4
--- /dev/null
+++ b/tests/integration/ignore_root_user_error/MODULE.bazel
@@ -0,0 +1,20 @@
+module(name = "ignore_root_user_error")
+
+bazel_dep(name = "rules_python", version = "0.0.0")
+local_path_override(
+ module_name = "rules_python",
+ path = "../../..",
+)
+
+bazel_dep(name = "submodule")
+local_path_override(
+ module_name = "submodule",
+ path = "submodule",
+)
+
+python = use_extension("@rules_python//python/extensions:python.bzl", "python")
+python.toolchain(
+ ignore_root_user_error = True,
+ python_version = "3.11",
+)
+use_repo(python, "rules_python_bzlmod_debug")
diff --git a/tests/integration/ignore_root_user_error/bzlmod_test.py b/tests/integration/ignore_root_user_error/bzlmod_test.py
new file mode 100644
index 0000000..62bc4f4
--- /dev/null
+++ b/tests/integration/ignore_root_user_error/bzlmod_test.py
@@ -0,0 +1,37 @@
+# Copyright 2024 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.
+
+import unittest
+import pathlib
+import json
+
+from python.runfiles import runfiles
+
+class BzlmodTest(unittest.TestCase):
+ def test_toolchains(self):
+ rf = runfiles.Create()
+ debug_path = pathlib.Path(rf.Rlocation(
+ "rules_python_bzlmod_debug/debug_info.json"
+ ))
+ debug_info = json.loads(debug_path.read_bytes())
+
+ expected = [
+ {'ignore_root_user_error': True, 'name': 'python_3_11', },
+ {'ignore_root_user_error': True, 'name': 'python_3_10'}
+ ]
+ self.assertCountEqual(debug_info["toolchains_registered"],
+ expected)
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/integration/ignore_root_user_error/submodule/BUILD.bazel b/tests/integration/ignore_root_user_error/submodule/BUILD.bazel
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/integration/ignore_root_user_error/submodule/BUILD.bazel
diff --git a/tests/integration/ignore_root_user_error/submodule/MODULE.bazel b/tests/integration/ignore_root_user_error/submodule/MODULE.bazel
new file mode 100644
index 0000000..f128709
--- /dev/null
+++ b/tests/integration/ignore_root_user_error/submodule/MODULE.bazel
@@ -0,0 +1,9 @@
+module(name = "submodule")
+
+bazel_dep(name = "rules_python", version = "0.0.0")
+
+python = use_extension("@rules_python//python/extensions:python.bzl", "python")
+python.toolchain(
+ ignore_root_user_error = False,
+ python_version = "3.10",
+)
diff --git a/tests/integration/ignore_root_user_error/submodule/WORKSPACE b/tests/integration/ignore_root_user_error/submodule/WORKSPACE
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/integration/ignore_root_user_error/submodule/WORKSPACE