Skip to content

Commit 8ecad9d

Browse files
shabanzdrickeylev
andauthored
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 <[email protected]>
1 parent b4d2c7b commit 8ecad9d

File tree

12 files changed

+200
-14
lines changed

12 files changed

+200
-14
lines changed

.bazelrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
# (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it)
55
# To update these lines, execute
66
# `bazel run @rules_bazel_integration_test//tools:update_deleted_packages`
7-
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
8-
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
7+
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
8+
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
99

1010
test --test_output=errors
1111

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ A brief description of the categories of changes:
124124
* (toolchains) Workspace builds register the py cc toolchain (bzlmod already
125125
was). This makes e.g. `//python/cc:current_py_cc_headers` Just Work.
126126
([#1669](https://github.com/bazelbuild/rules_python/issues/1669))
127+
* (bzlmod python.toolchain) The value of `ignore_root_user_error` is now decided
128+
by the root module only.
129+
([#1658](https://github.com/bazelbuild/rules_python/issues/1658))
127130

128131
### Added
129132

python/extensions/BUILD.bazel

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,8 @@ bzl_library(
3535
name = "python_bzl",
3636
srcs = ["python.bzl"],
3737
visibility = ["//:__subpackages__"],
38-
deps = ["//python/private/bzlmod:python_bzl"],
38+
deps = [
39+
"//python/private:util_bzl",
40+
"//python/private/bzlmod:python_bzl",
41+
],
3942
)

python/private/bzlmod/python.bzl

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
load("//python:repositories.bzl", "python_register_toolchains")
1818
load("//python/private:toolchains_repo.bzl", "multi_toolchain_aliases")
19+
load("//python/private:util.bzl", "IS_BAZEL_6_4_OR_HIGHER")
1920
load(":pythons_hub.bzl", "hub_repo")
2021

2122
# This limit can be increased essentially arbitrarily, but doing so will cause a rebuild of all
@@ -43,14 +44,14 @@ def _left_pad_zero(index, length):
4344
def _print_warn(msg):
4445
print("WARNING:", msg)
4546

46-
def _python_register_toolchains(name, toolchain_attr, module):
47+
def _python_register_toolchains(name, toolchain_attr, module, ignore_root_user_error):
4748
"""Calls python_register_toolchains and returns a struct used to collect the toolchains.
4849
"""
4950
python_register_toolchains(
5051
name = name,
5152
python_version = toolchain_attr.python_version,
5253
register_coverage_tool = toolchain_attr.configure_coverage_tool,
53-
ignore_root_user_error = toolchain_attr.ignore_root_user_error,
54+
ignore_root_user_error = ignore_root_user_error,
5455
)
5556
return struct(
5657
python_version = toolchain_attr.python_version,
@@ -59,6 +60,13 @@ def _python_register_toolchains(name, toolchain_attr, module):
5960
)
6061

6162
def _python_impl(module_ctx):
63+
if module_ctx.os.environ.get("RULES_PYTHON_BZLMOD_DEBUG", "0") == "1":
64+
debug_info = {
65+
"toolchains_registered": [],
66+
}
67+
else:
68+
debug_info = None
69+
6270
# The toolchain_info structs to register, in the order to register them in.
6371
# NOTE: The last element is special: it is treated as the default toolchain,
6472
# so there is special handling to ensure the last entry is the correct one.
@@ -72,6 +80,13 @@ def _python_impl(module_ctx):
7280
# Map of string Major.Minor to the toolchain_info struct
7381
global_toolchain_versions = {}
7482

83+
ignore_root_user_error = None
84+
85+
# if the root module does not register any toolchain then the
86+
# ignore_root_user_error takes its default value: False
87+
if not module_ctx.modules[0].tags.toolchain:
88+
ignore_root_user_error = False
89+
7590
for mod in module_ctx.modules:
7691
module_toolchain_versions = []
7792

@@ -84,16 +99,27 @@ def _python_impl(module_ctx):
8499
_fail_duplicate_module_toolchain_version(toolchain_version, mod.name)
85100
module_toolchain_versions.append(toolchain_version)
86101

87-
# Only the root module and rules_python are allowed to specify the default
88-
# toolchain for a couple reasons:
89-
# * It prevents submodules from specifying different defaults and only
90-
# one of them winning.
91-
# * rules_python needs to set a soft default in case the root module doesn't,
92-
# e.g. if the root module doesn't use Python itself.
93-
# * The root module is allowed to override the rules_python default.
94102
if mod.is_root:
103+
# Only the root module and rules_python are allowed to specify the default
104+
# toolchain for a couple reasons:
105+
# * It prevents submodules from specifying different defaults and only
106+
# one of them winning.
107+
# * rules_python needs to set a soft default in case the root module doesn't,
108+
# e.g. if the root module doesn't use Python itself.
109+
# * The root module is allowed to override the rules_python default.
110+
95111
# A single toolchain is treated as the default because it's unambiguous.
96112
is_default = toolchain_attr.is_default or len(mod.tags.toolchain) == 1
113+
114+
# Also only the root module should be able to decide ignore_root_user_error.
115+
# Modules being depended upon don't know the final environment, so they aren't
116+
# in the right position to know or decide what the correct setting is.
117+
118+
# If an inconsistency in the ignore_root_user_error among multiple toolchains is detected, fail.
119+
if ignore_root_user_error != None and toolchain_attr.ignore_root_user_error != ignore_root_user_error:
120+
fail("Toolchains in the root module must have consistent 'ignore_root_user_error' attributes")
121+
122+
ignore_root_user_error = toolchain_attr.ignore_root_user_error
97123
elif mod.name == "rules_python" and not default_toolchain:
98124
# We don't do the len() check because we want the default that rules_python
99125
# sets to be clearly visible.
@@ -128,8 +154,14 @@ def _python_impl(module_ctx):
128154
toolchain_name,
129155
toolchain_attr,
130156
module = mod,
157+
ignore_root_user_error = ignore_root_user_error,
131158
)
132159
global_toolchain_versions[toolchain_version] = toolchain_info
160+
if debug_info:
161+
debug_info["toolchains_registered"].append({
162+
"ignore_root_user_error": ignore_root_user_error,
163+
"name": toolchain_name,
164+
})
133165

134166
if is_default:
135167
# This toolchain is setting the default, but the actual
@@ -192,6 +224,12 @@ def _python_impl(module_ctx):
192224
},
193225
)
194226

227+
if debug_info != None:
228+
_debug_repo(
229+
name = "rules_python_bzlmod_debug",
230+
debug_info = json.encode_indent(debug_info),
231+
)
232+
195233
def _fail_duplicate_module_toolchain_version(version, module):
196234
fail(("Duplicate module toolchain version: module '{module}' attempted " +
197235
"to use version '{version}' multiple times in itself").format(
@@ -220,6 +258,14 @@ def _fail_multiple_default_toolchains(first, second):
220258
second = second,
221259
))
222260

261+
def _get_bazel_version_specific_kwargs():
262+
kwargs = {}
263+
264+
if IS_BAZEL_6_4_OR_HIGHER:
265+
kwargs["environ"] = ["RULES_PYTHON_BZLMOD_DEBUG"]
266+
267+
return kwargs
268+
223269
python = module_extension(
224270
doc = """Bzlmod extension that is used to register Python toolchains.
225271
""",
@@ -263,7 +309,16 @@ A toolchain's repository name uses the format `python_{major}_{minor}`, e.g.
263309
),
264310
"ignore_root_user_error": attr.bool(
265311
default = False,
266-
doc = "Whether the check for root should be ignored or not. This causes cache misses with .pyc files.",
312+
doc = """\
313+
If False, the Python runtime installation will be made read only. This improves
314+
the ability for Bazel to cache it, but prevents the interpreter from creating
315+
pyc files for the standard library dynamically at runtime as they are loaded.
316+
317+
If True, the Python runtime installation is read-write. This allows the
318+
interpreter to create pyc files for the standard library, but, because they are
319+
created as needed, it adversely affects Bazel's ability to cache the runtime and
320+
can result in spurious build failures.
321+
""",
267322
mandatory = False,
268323
),
269324
"is_default": attr.bool(
@@ -279,4 +334,23 @@ A toolchain's repository name uses the format `python_{major}_{minor}`, e.g.
279334
},
280335
),
281336
},
337+
**_get_bazel_version_specific_kwargs()
338+
)
339+
340+
_DEBUG_BUILD_CONTENT = """
341+
package(
342+
default_visibility = ["//visibility:public"],
343+
)
344+
exports_files(["debug_info.json"])
345+
"""
346+
347+
def _debug_repo_impl(repo_ctx):
348+
repo_ctx.file("BUILD.bazel", _DEBUG_BUILD_CONTENT)
349+
repo_ctx.file("debug_info.json", repo_ctx.attr.debug_info)
350+
351+
_debug_repo = repository_rule(
352+
implementation = _debug_repo_impl,
353+
attrs = {
354+
"debug_info": attr.string(),
355+
},
282356
)

python/private/util.bzl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,16 @@ IS_BAZEL_7_OR_HIGHER = hasattr(native, "starlark_doc_extract")
8989
# Bazel 5.4 has a bug where every access of testing.ExecutionInfo is a
9090
# different object that isn't equal to any other. This is fixed in bazel 6+.
9191
IS_BAZEL_6_OR_HIGHER = testing.ExecutionInfo == testing.ExecutionInfo
92+
93+
_marker_rule_to_detect_bazel_6_4_or_higher = rule(implementation = lambda ctx: None)
94+
95+
# Bazel 6.4 and higher have a bug fix where rule names show up in the str()
96+
# of a rule. See
97+
# https://github.com/bazelbuild/bazel/commit/002490b9a2376f0b2ea4a37102c5e94fc50a65ba
98+
# https://github.com/bazelbuild/bazel/commit/443cbcb641e17f7337ccfdecdfa5e69bc16cae55
99+
# This technique is done instead of using native.bazel_version because,
100+
# under stardoc, the native.bazel_version attribute is entirely missing, which
101+
# prevents doc generation from being able to correctly generate docs.
102+
IS_BAZEL_6_4_OR_HIGHER = "_marker_rule_to_detect_bazel_6_4_or_higher" in str(
103+
_marker_rule_to_detect_bazel_6_4_or_higher,
104+
)

tests/integration/ignore_root_user_error/.bazelrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
common --action_env=RULES_PYTHON_BZLMOD_DEBUG=1
2+
common --lockfile_mode=off
13
test --test_output=errors
24

35
# Windows requires these for multi-python support:
Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,32 @@
1-
load("@rules_python//python:defs.bzl", "py_test")
1+
# Copyright 2024 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
load("@rules_python//python:py_test.bzl", "py_test")
16+
load("@rules_python//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility
217

318
py_test(
419
name = "foo_test",
520
srcs = ["foo_test.py"],
621
visibility = ["//visibility:public"],
722
)
23+
24+
py_test(
25+
name = "bzlmod_test",
26+
srcs = ["bzlmod_test.py"],
27+
data = [
28+
"@rules_python//python/runfiles",
29+
"@rules_python_bzlmod_debug//:debug_info.json",
30+
],
31+
target_compatible_with = [] if BZLMOD_ENABLED else ["@platforms//:incompatible"],
32+
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module(name = "ignore_root_user_error")
2+
3+
bazel_dep(name = "rules_python", version = "0.0.0")
4+
local_path_override(
5+
module_name = "rules_python",
6+
path = "../../..",
7+
)
8+
9+
bazel_dep(name = "submodule")
10+
local_path_override(
11+
module_name = "submodule",
12+
path = "submodule",
13+
)
14+
15+
python = use_extension("@rules_python//python/extensions:python.bzl", "python")
16+
python.toolchain(
17+
ignore_root_user_error = True,
18+
python_version = "3.11",
19+
)
20+
use_repo(python, "rules_python_bzlmod_debug")
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright 2024 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import unittest
16+
import pathlib
17+
import json
18+
19+
from python.runfiles import runfiles
20+
21+
class BzlmodTest(unittest.TestCase):
22+
def test_toolchains(self):
23+
rf = runfiles.Create()
24+
debug_path = pathlib.Path(rf.Rlocation(
25+
"rules_python_bzlmod_debug/debug_info.json"
26+
))
27+
debug_info = json.loads(debug_path.read_bytes())
28+
29+
expected = [
30+
{'ignore_root_user_error': True, 'name': 'python_3_11', },
31+
{'ignore_root_user_error': True, 'name': 'python_3_10'}
32+
]
33+
self.assertCountEqual(debug_info["toolchains_registered"],
34+
expected)
35+
36+
if __name__ == "__main__":
37+
unittest.main()

tests/integration/ignore_root_user_error/submodule/BUILD.bazel

Whitespace-only changes.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module(name = "submodule")
2+
3+
bazel_dep(name = "rules_python", version = "0.0.0")
4+
5+
python = use_extension("@rules_python//python/extensions:python.bzl", "python")
6+
python.toolchain(
7+
ignore_root_user_error = False,
8+
python_version = "3.10",
9+
)

tests/integration/ignore_root_user_error/submodule/WORKSPACE

Whitespace-only changes.

0 commit comments

Comments
 (0)