bazelbuild / bazelbuild/bazel

Option to make all ctx.actions.run_shell actions in this run to have a hermetic PATH

Open
#26,326 7 comments 5 reactions 0 assignees View on GitHub
P3 team-Rules-CPP type: feature request
Dominant language
Java
Stars
25.8k
Forks
4.6k
Avg merge
2d 20h
Merged PRs (30d)
72

Description

### Description of the feature request:

Allow the user to create a hermetic environment for **ALL** ctx.actions.run_shell in the current build run.

### Which category does this issue belong to?

Core, Local Execution, Rules API

### What underlying problem are you trying to solve with this feature?

# Problem statement

Suppose we have this simple rule:

```
def _impl(ctx):
f = ctx.actions.declare_file(ctx.label.name)
ctx.actions.run_shell(
inputs = [],
outputs = [f],
command = """
echo $PATH >&2
touch {}
""".format(f.path)
)
return DefaultInfo(files = depset([f]))
foo = rule(
implementation = _impl,
)
```

Building a target of this rule with `--incompatible_strict_action_env` will yield the following result (Bazel 8.2.1, on Linux)
```
/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:.
```

This means any commands executed by `ctx.actions.run_shell()` will by default use non-hermetic tools in PATH. In the above example, `touch` is `/usr/bin/touch` from the host machine. This is the case even with `--incompatible_strict_action_env`; the flag limits the list in PATH, but it is still non-hermetic.

The developer of the `foo()` rule could change all `ctx.actions.run_shell()` in their rulesets to set up a `PATH` before executing the command. However, that does not include dependencies. For example, bazel_skylib's copy_file() rule is also not hermetic:

https://github.com/bazelbuild/bazel-skylib/blob/223e4e945801dfbc0bfa31d0900196f5bb54b0fc/rules/private/copy_file_private.bzl#L56

As a user of bazel_skylib, I can't control what `cp` program that `copy_file()` is using. (There are some workarounds; see below)

Another example: if a module dependency has a `genrule()` in it , it is also likely that the `genrule()` will be built with tools from the host.

# Workaround

Workaround 1: One could use third-party sandboxes (e.g. docker) to control which tools would be provided to Bazel actions.

Workaround 2: One could use `--action_env=PATH=...` to control the list of tools that any action uses. For example, one could:

1. Set up a `/tmp/contained_path` directory with symlinks to a list of tools, e.g. `cp` in the above example
2. Then `bazel build --action_env=PATH=/tmp/contained_path [targets]`

Both workarounds are not only hacky, non-bazellic, but also can't recognize tools that are built from sources. For example, if I don't have the prebuilt `cp` program, but a source file and a `cc_binary()` rule that builds it, then I'll need to:

1. `bazel build //:cp`
2. Create symlink `/tmp/contained_path/cp` -> `bazel-bin/cp`
3. Execute the real build for my targets with `--action_env=PATH=/tmp/contained_path`.

# Proposed feature

The user should be able to register a toolchain that contains a set of tools. These tools will be in PATH for **all** ctx.actions.run_shell(); all other PATH's in ctx.actions.run_shell() should be eliminated.

A more concrete example in pseudocode is below. I use `sh_toolchain()` here because I think it is natrual that this is combined with rules_shell, but it doesn't have to be; it can be a separate toolchain.

```
cc_binary(name = "cp", ...) # binaries built from sources
native_binary(name = "lz4", ...) # prebuilt binaries
sh_toolchain(
name = "my_hermetic_shell_toolchain",
path = ...,
tools = [":cp", ":lz4", ...],
)
toolchain(
name = "my_hermetic_shell_toolchain_toolchain",
toolchain = ":my_hermetic_shell_toolchain",
toolchain_type = "@rules_shell//shell:toolchain_type",
)
```

Then, with `my_hermetic_shell_toolchain_toolchain` in `register_toolchains()`, any `ctx.actions.run_shell()` should have a sanitized `PATH` that does not point to any tools in the host machine. `copy_file()` above will use `cp` built from this `cc_binary()` rule.

In the above example, since sh_toolchain() contains the path to a shell, ctx.actions.run_shell() should probably use the same shell.

# Backwards Compatibility

If this feature were to be built based on `sh_toolchain()` then it may need to be hidden behind an `--incompatible_run_shell_action_uses_sh_toolchain`, for example.

If this feature were to be built on top of a new toolchain type, then I don't think any flag would be necessary; Bazel can just fallback to its old behavior when the new toolchain type is not registered.

### Which operating system are you running Bazel on?

Linux

### What is the output of `bazel info release`?

release 8.2.1

### If `bazel info release` returns `development version` or `(@non-git)`, tell us how you built Bazel.

_No response_

### What's the output of `git remote get-url origin; git rev-parse HEAD` ?

```text

```

### Have you found anything relevant by searching the web?

https://github.com/bazelbuild/bazel/issues/5265 ("shell toolchain, design: including common bintools") is related. The bug is closed, but comments like

https://github.com/bazelbuild/bazel/issues/5265#issuecomment-1381126111

https://github.com/bazelbuild/bazel/issues/5265#issuecomment-1384783692

are still relevant, and I think these comments would be addressed if `ctx.actions.run_shell` respects `@bazel_tools//tools/sh:toolchain_type` or `@rules_shell//shell:toolchain_type` or some toolchain.

### Any other information, logs, or outputs that you want to share?

_No response_

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.