Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(bzlmod): users have no way to opt out of hermetic toolchain downloads due to precompiling toolchain #1967

Closed
aignas opened this issue Jun 14, 2024 · 7 comments · Fixed by #1968

Comments

@aignas
Copy link
Collaborator

aignas commented Jun 14, 2024

It seems that users will download the automatically registered precompile toolchain using the hermetic interpreter even if they would like to use the autodetecting toolchain.

For analysis see comments below and the attached PR for a possible beginning of a fix.

Initial description: The logic change in python/private/py_toolchain_suite.bzl in #1837 probably results in the autodetecting toolchain not being selected.

The toolchain registration is based on the target_settings and constraint_values. If those are the same, then the last registered toolchain overwrites the previously added one. In bazelbuild/bazel#22718 there is usage of an extra toolchain via:

       --extra_toolchains=@bazel_tools//tools/python:autodetecting_toolchain \ 

This means that in the previous logic (which would be sometimes incorrect) the autodetecting toolchain would overwrite any of the default toolchain that was registered by rules_python in the default case.

With the change, we now have 2 toolchains that match the default case when the python_version config setting value is unset - the hermetic and the autodetecting one and the hermetic one is more specialized.

Fixing this without reverting idea 1

A likely fixed could be to auto-register the auto-detecting toolchain and have a config flag that allows the user to select the autodetecting toolchain explicitly over the hermetic toolchain and to fallback to the auto-detecting toolchain if allowed and if no toolchain matches.

Fixing this without reverting idea 2

Use the rules_python_config or the python extension to write data so that the default python_version setting can be set instead. That way all python toolchains would have a version and there would be no special case of a hermetic toolchain which matches "" config setting.

@aignas
Copy link
Collaborator Author

aignas commented Jun 14, 2024

I might try the idea 2. first as that simplifies the code in general.

EDIT: It seems that the full answer may be more nuanced because the toolchain debug log shows:

$ bazel build --@rules_python//python/config_settings:python_version="" @dev_pip//charset_normalizer --toolchain_resolution_debug='python' --extra_toolchains=@bazel_tools//tools/python:autodetecting_toolchain

WARNING: Build option --//python/config_settings:python_version has changed, discarding analysis cache (this can be expensive, see https://bazel.build/advanced/performance/iteration-speed).
INFO: ToolchainResolution: Target platform @@local_config_platform//:host: Selected execution platform @@local_config_platform//:host,
INFO: ToolchainResolution: Performing resolution of @@bazel_tools//tools/python:toolchain_type for target platform @@local_config_platform//:host
      ToolchainResolution:   Toolchain @@bazel_tools//tools/python:_autodetecting_py_runtime_pair is compatible with target plaform, searching for execution platforms:
      ToolchainResolution:     Compatible execution platform @@local_config_platform//:host
      ToolchainResolution:   All execution platforms have been assigned a @@bazel_tools//tools/python:toolchain_type toolchain, stopping
      ToolchainResolution: Recap of selected @@bazel_tools//tools/python:toolchain_type toolchains for target platform @@local_config_platform//:host:
      ToolchainResolution:   Selected @@bazel_tools//tools/python:_autodetecting_py_runtime_pair to run on execution platform @@local_config_platform//:host
INFO: ToolchainResolution: Performing resolution of //python:exec_tools_toolchain_type for target platform @@local_config_platform//:host
      ToolchainResolution:   Toolchain @@_main~python~python_3_11_aarch64-apple-darwin//:py_exec_tools_toolchain is compatible with target plaform, searching for execution platforms:
      ToolchainResolution:     Incompatible execution platform @@local_config_platform//:host; mismatching values: osx, aarch64
      ToolchainResolution:   Toolchain @@_main~python~python_3_11_aarch64-unknown-linux-gnu//:py_exec_tools_toolchain is compatible with target plaform, searching for execution platforms:
      ToolchainResolution:     Incompatible execution platform @@local_config_platform//:host; mismatching values: aarch64
      ToolchainResolution:   Toolchain @@[unknown repo 'python_3_11_armv7-unknown-linux-gnu' requested from @@_main~python~pythons_hub]//:py_exec_tools_toolchain is compatible with target plaform, searching for execution platforms:
      ToolchainResolution:     Incompatible execution platform @@local_config_platform//:host; mismatching values: armv7
      ToolchainResolution:   Toolchain @@_main~python~python_3_11_ppc64le-unknown-linux-gnu//:py_exec_tools_toolchain is compatible with target plaform, searching for execution platforms:
      ToolchainResolution:     Incompatible execution platform @@local_config_platform//:host; mismatching values: ppc
      ToolchainResolution:   Toolchain @@[unknown repo 'python_3_11_riscv64-unknown-linux-gnu' requested from @@_main~python~pythons_hub]//:py_exec_tools_toolchain is compatible with target plaform, searching for execution platforms:
      ToolchainResolution:     Incompatible execution platform @@local_config_platform//:host; mismatching values: riscv64
      ToolchainResolution:   Toolchain @@_main~python~python_3_11_s390x-unknown-linux-gnu//:py_exec_tools_toolchain is compatible with target plaform, searching for execution platforms:
      ToolchainResolution:     Incompatible execution platform @@local_config_platform//:host; mismatching values: s390x
      ToolchainResolution:   Toolchain @@_main~python~python_3_11_x86_64-apple-darwin//:py_exec_tools_toolchain is compatible with target plaform, searching for execution platforms:
      ToolchainResolution:     Incompatible execution platform @@local_config_platform//:host; mismatching values: osx
      ToolchainResolution:   Toolchain @@_main~python~python_3_11_x86_64-pc-windows-msvc//:py_exec_tools_toolchain is compatible with target plaform, searching for execution platforms:
      ToolchainResolution:     Incompatible execution platform @@local_config_platform//:host; mismatching values: windows
      ToolchainResolution:   Toolchain @@_main~python~python_3_11_x86_64-unknown-linux-gnu//:py_exec_tools_toolchain is compatible with target plaform, searching for execution platforms:
      ToolchainResolution:     Compatible execution platform @@local_config_platform//:host
      ToolchainResolution:   All execution platforms have been assigned a //python:exec_tools_toolchain_type toolchain, stopping
      ToolchainResolution: Recap of selected //python:exec_tools_toolchain_type toolchains for target platform @@local_config_platform//:host:
      ToolchainResolution:   Selected @@_main~python~python_3_11_x86_64-unknown-linux-gnu//:py_exec_tools_toolchain to run on execution platform @@local_config_platform//:host
INFO: ToolchainResolution: Target platform @@local_config_platform//:host: Selected execution platform @@local_config_platform//:host, type //python:exec_tools_toolchain_type -> toolchain @@_main~python~python_3_11_x86_64-unknown-linux-gnu//:py_exec_tools_toolchain, type @@bazel_tools//tools/python:toolchain_type -> toolchain @@bazel_tools//tools/python:_autodetecting_py_runtime_pair
INFO: ToolchainResolution: Target platform @@local_config_platform//:host: Selected execution platform @@local_config_platform//:host,
INFO: Analyzed target @@_main~pip_internal~dev_pip//charset_normalizer:charset_normalizer (46 packages loaded, 1530 targets configured).
INFO: Found 1 target...
Target @@_main~pip_internal~dev_pip_311_charset_normalizer_cp311_cp311_manylinux_2_17_x86_64_753f10e8//:pkg up-to-date (nothing to build)
INFO: Elapsed time: 4.917s, Critical Path: 0.01s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action

The important lines are:

$ bazel build --@rules_python//python/config_settings:python_version="" @dev_pip//charset_normalizer --toolchain_resolution_debug='python' --extra_toolchains=@bazel_tools//tools/python:autodetecting_toolchain

WARNING: Build option --//python/config_settings:python_version has changed, discarding analysis cache (this can be expensive, see https://bazel.build/advanced/performance/iteration-speed).
INFO: ToolchainResolution: Target platform @@local_config_platform//:host: Selected execution platform @@local_config_platform//:host,
INFO: ToolchainResolution: Performing resolution of @@bazel_tools//tools/python:toolchain_type for target platform @@local_config_platform//:host
      ...
      ToolchainResolution: Recap of selected @@bazel_tools//tools/python:toolchain_type toolchains for target platform @@local_config_platform//:host:
      ToolchainResolution:   Selected @@bazel_tools//tools/python:_autodetecting_py_runtime_pair to run on execution platform @@local_config_platform//:host

INFO: ToolchainResolution: Performing resolution of //python:exec_tools_toolchain_type for target platform @@local_config_platform//:host
      ...
      ToolchainResolution: Recap of selected //python:exec_tools_toolchain_type toolchains for target platform @@local_config_platform//:host:
      ToolchainResolution:   Selected @@_main~python~python_3_11_x86_64-unknown-linux-gnu//:py_exec_tools_toolchain to run on execution platform @@local_config_platform//:host
INFO: ToolchainResolution: Target platform @@local_config_platform//:host: Selected execution platform @@local_config_platform//:host, type //python:exec_tools_toolchain_type -> toolchain @@_main~python~python_3_11_x86_64-unknown-linux-gnu//:py_exec_tools_toolchain, type @@bazel_tools//tools/python:toolchain_type -> toolchain @@bazel_tools//tools/python:_autodetecting_py_runtime_pair

It seems that we resolve 2 toolchains, one for exec_tools_toolchain_type and one for the regular toolchain type. I thought that the exec_tools_toolchain_type should not be present, because that feature is disabled? I am not sure....

@aignas
Copy link
Collaborator Author

aignas commented Jun 14, 2024

Yeah, I think my suspicion is correct, see:

$ bazel cquery 'deps(@dev_pip//charset_normalizer)' --transitions=lite --extra_toolchains=@bazel_tools//tools/python:autodetecting_toolchain | grep "toolchain dependency"

Computing main repo mapping:
Loading:
Loading: 0 packages loaded
Analyzing: target @@_main~pip_internal~dev_pip//charset_normalizer:charset_normalizer (0 packages loaded, 0 targets configured)
INFO: Analyzed target @@_main~pip_internal~dev_pip//charset_normalizer:charset_normalizer (1 packages loaded, 80 targets configured).
INFO: Found 1 target...
  [toolchain dependency]#@@_main~python~python_3_11_x86_64-unknown-linux-gnu//:py_exec_tools_toolchain#NoTransition ->
  [toolchain dependency]#@bazel_tools//tools/python:_autodetecting_py_runtime_pair#NoTransition ->
INFO: Elapsed time: 0.246s, Critical Path: 0.00s
INFO: 0 processes.
INFO: Build completed successfully, 0 total actions

It seems that because the toolchain for python file compilation exists, we'll depend on it and there is no way to get rid of it in the dependency graph other than:

  1. Comment the native.toolchain usage in py_toolchian_suite.bzl
  2. Create target_settings which would switch conditionally on the flag values.
  3. Create an autodetecting variant of that.

aignas added a commit that referenced this issue Jun 14, 2024
With this change we set the `target_settings` to something that is not
going to be ever matched if the `precompile` flag is not enabled. Users
wanting to use the `precompile` feature will need to set the
config_setting value to `enabled` instead of `auto`.

Fixes #1967
aignas added a commit that referenced this issue Jun 14, 2024
With this change we set the `target_settings` to something that is not
going to be ever matched if the `precompile` flag is not enabled. Users
wanting to use the `precompile` feature will need to set the
config_setting value to `enabled` instead of `auto`.

Fixes #1967
@aignas aignas changed the title (bzlmod): autodetecting toolchain is not being selected as a fallback (bzlmod): users have no way to opt out of hermetic toolchain downloads due to precompiling toolchain Jun 14, 2024
@rickeylev
Copy link
Contributor

rickeylev commented Jun 15, 2024

I need to verify what I say below, but with the logs aignas posted, I'm pretty sure this is what's happening.

It seems that we resolve 2 toolchains, one for exec_tools_toolchain_type and one for the regular toolchain type. I thought that the exec_tools_toolchain_type should not be present, because that feature is disabled?

Yeah, I agree with your analysis and expectation. The behavior I expect to see is: if precompiling isn't enabled, then the exec toolchain shouldn't be resolved, because those actions are never registered.

In case it wasn't clear, the the exec_tools toolchain has its own reference to an interpreter. So this is where it's getting the hermetic runtime from. This is so that it has an exec-config interpreter (and is thus valid to run in an action) that has a Python version matching the target-config constraints.

Removing the exec-config-interpreter from the exec_tools toolchain would be fine with me, however, I'm not sure if there's another way to get the correctly configured interpreter. Ideally, whatever binary the exec-config precompiler attribute points to would lookup the regular target-platform toolchain and use that, but I'm not sure if the constraints propagate correctly through such a relationship (if the precompiler is supposed to generate e.g. 3.11 pyc files, we need to make sure the 3.11 constraint is carried over to its toolchain resolution).

Create an autodetecting toolchain for the exec tools toolchain

Yeha, I like this idea, too. Telling people to do e.g. --extra_toolchains=@rules_python//toolchains/autodetecting:all would be a decent way to let them specify using an autodetecting one, while giving us the freedom to change our toolchain types. That said, I'm -1 on this idea as a solution for 0.33.x because it requires using a new API in the same release the API is introduced. I'd much rather a solution where people can upgrade to 0.33 without having to change the APIs they interact with.

use a select to disable the toolchain (PR 1968)

This idea has some appeal as an interim fix if the ideas below don't pan out. A variation of this idea would be a separate flag that controls registration instead of the extra config_setting hoops the PR jumps through. Precompiling is a new feature you have to opt-into anyways.

A few ideas to look into:

First idea: in common_bazel.bzl, the first thing it does is an exec tools toolchain lookup. Maybe only doing that access if precompiling is going to happen will fix the issue? The docs about auto exec groups gave me the impression that something magical was happening where AEGs caused Bazel to defer the actual resolution normally caused by ctx.toolchains when the value was later used in a ctx.actions.run() call using AEGs.

Next idea: maybe any access of ctx.toolchains is causing the exec tools toolchain to be resolved? Given how toolchain resolution is documented this might be the case? I don't see how it can enforce the "every toolchain of the rule uses the same constraints" unless it resolves all the toolchain types whenever ctx.toolchains is accessed, though. This seems at odds with AEGs, though? I dunno want to make of this discrepancy. In any case, maybe we have to also update any other toolchain lookups to also use AEGs (i.e. set toolchain=None)? If this is the case, maybe switching to explicit execution groups might help.

aignas added a commit that referenced this issue Jun 15, 2024
With this we can finally be correct with how we deal with the precompile
toolchains. This is done in two ways:
1. Add a config_setting that explicitly enableds/disables the precompile
   toolchains.
2. Add an autodetecting toolchain for precompile in cases where users
   may want to use the autodetecting toolchain and we would like to
   ensure that the hermetic toolchain is not pulled in because it exists
   and is used in the target.

To keep the behaviour backwards compatible, add an alias to the
autodetecting toolchain with a deprecation notice. The API is new in
0.33.0 and it is unfortunate that we have to do this, but the users
should still be able to use the code as is.

Fixes #1967.
@aignas
Copy link
Collaborator Author

aignas commented Jun 15, 2024

Thanks @rickeylev for the thorough analysis, I updated #1968 with:

  • Moved the toolchain lookup down the call stack, but observed no difference, not sure if we should keep it. It seems that the toolchain lookup later (only if the precompile is enabled) did not do anything, so I think we have to go the route of autodetecting toolchain for precompile and a config flag to disable the toolchain usage.
  • autodetecting_toolchain for the precompile got added and I moved everything to python/autodetecting_toolchain with addition of the alias to keep the behaviour backwards compatible. I am not sure if we should include this for 0.33.2 or only for 0.34.0, but making a single PR was easier in this case.
  • config setting to explicitly enable precompile toolchains.
  • updates the docs.

I am curious if you would like to leave the ticket open to look into AEGs or if we should create a separate ticket here.

@rickeylev
Copy link
Contributor

Argh, yeah. It looks it doesn't matter if a rule uses the toolchains or not -- simply having them listed attempts resolving them, even if they're optional. This behavior makes some sense, but is kind of annoying in this case. In this case, we essentially have an erroneously defined optional toolchain, and are fine with pretending it doesn't exist.

I tried using an exec group for the toolchain, but the behavior is the same. Whether the toolchain is used or not, it gets resolved. This is surprising and seems wrong? Exec groups are tied to actions, so if there aren't any actions triggering it, then resolving it is wasted effort.

Ah, well, this behavior is out of our control, so just gotta roll with it.

So yeah, the only trick I can think of is for the toolchain to be incompatible (resolution is still happening, it just doesn't traverse into the toolchain implementation, so the details of the toolchain implementation don't get resolved).

I'm going to see if I can make the exec tools intepreter re-use the target toolchain interpreter. If that doesn't look promising, we'll go with the flag approach.

Another thing I noticed is the _use_auto_exec_groups attribute is missing; I'll send a PR to add that later.

@rickeylev
Copy link
Contributor

I prototyped an alternative that looks promising. Basically, a cfg=exec attribute that looks up the target toolchain and returns it. This avoids the exec toolchain needing its own reference to an interpreter. I'll clean it up tonight and see how it looks.

@rickeylev
Copy link
Contributor

Ah, dang. Well my idea works to allow the exec_tools toolchain to use the target toolchain to find the runtime (the net effect being, :autodetecting_toolchain would also determine the python used for e.g. precompiling). However, I forgot that's only half the problem: it's still a hermetic-repo that is being chosen during toolchain resolution, so its repo will still be instantiated (and the ensuing "download python" commands run). The only way to avoid that is to not choose that toolchain, i.e. what Ignas's PR does (either marking the toolchain incompatible, or providing an alternative toolchain to take priority). I'll go review his PR.

github-merge-queue bot pushed a commit that referenced this issue Jun 19, 2024
This makes the exec tools toolchain disabled by default to prevent
toolchain resolution
from matching it and inadvertently pulling in a dependency on the
hermetic runtimes.
While the hermetic runtime wouldn't actually be used (precompiling is
disabled
by default), the dependency triggered downloading of the runtimes, which
breaks
environments which forbid remote downloads they haven't vetted (such a
case is
Bazel's own build process).

To fix this, a flag is added to control if the exec tools toolchain is
enabled or not.
When disabled (the default), the toolchain won't match, and the remote
dependency isn't
triggered.

Fixes #1967.

---------

Co-authored-by: Richard Levasseur <rlevasseur@google.com>
aignas added a commit that referenced this issue Jun 19, 2024
This makes the exec tools toolchain disabled by default to prevent
toolchain resolution
from matching it and inadvertently pulling in a dependency on the
hermetic runtimes.
While the hermetic runtime wouldn't actually be used (precompiling is
disabled
by default), the dependency triggered downloading of the runtimes, which
breaks
environments which forbid remote downloads they haven't vetted (such a
case is
Bazel's own build process).

To fix this, a flag is added to control if the exec tools toolchain is
enabled or not.
When disabled (the default), the toolchain won't match, and the remote
dependency isn't
triggered.

Fixes #1967.

Cherry-pick of cf1f36d.

---------

Co-authored-by: Richard Levasseur <rlevasseur@google.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants