docs: expand and update documentation on `use_execroot_entry_point` (#2872)
---
### Changes are visible to end-users: yes
- Searched for relevant documentation and updated as needed: yes
- Breaking change (forces users to change their own code or config): no
- Suggested release notes appear below: no
### Test plan
diff --git a/docs/use_execroot_entry_point.md b/docs/use_execroot_entry_point.md
index 42e97ea..84f8bf3 100644
--- a/docs/use_execroot_entry_point.md
+++ b/docs/use_execroot_entry_point.md
@@ -1,15 +1,18 @@
# use_execroot_entry_point
This page describes the `use_execroot_entry_point` option on `js_run_binary`
-and provides guidance on when to use each value.
+and provides guidance on when to use each value. The short version is that
+`use_execroot_entry_point=True` sets up a directory layout that is more
+friendly to some JavaScript tools, but at the expense of a performance hit and
+issues with cross-platform builds.
## Background
When a `js_binary` is used as a tool in `js_run_binary`, Bazel runs it as a
build action on the exec platform. The execroot is the root of the build
sandbox; beneath it sits `bazel-out/`, which contains output directories for
-both the exec and target configurations. The tool's sources can therefore appear
-in up to three places:
+both the exec and target configurations. The tool's sources can therefore
+potentially appear in up to three places:
- **Exec-platform bin** (`bazel-out/<exec-cfg>/bin/`): where build artifacts for
the exec platform land.
@@ -17,21 +20,23 @@
where the tool's runtime dependencies (including `node_modules`) are
symlinked and made available to the build action.
- **Target-platform bin** (`bazel-out/<target-cfg>/bin/`): where the `srcs` of
- the `js_run_binary` action land.
+ the `js_run_binary` action land. This is also the default working directory
+ for the build action, though it can be adjusted via the `chdir` attribute.
-When Node.js resolves `require()`, it walks up the directory tree looking for
-`node_modules`. If the working directory is somewhere that can see
-`node_modules` from both the exec output tree and the runfiles tree, the same
-package can resolve from two different paths, which can cause subtle bugs.
+Resolving the same package in more than one location can result in subtle bugs,
+so this is a potential danger here given that the same sources can appear in up
+to three places. As described below, `use_execroot_entry_point = True`
+addresses the problem by keeping all sources in the target-platform bin
+directory, but this has downsides.
## What `use_execroot_entry_point` does
**`use_execroot_entry_point = True` (the current default):**
-The tool's runfiles are hoisted into `srcs`, which causes them to be rebuilt in
-the target configuration and land in the target-platform bin directory. The
-entry point used is the one in that output tree (the "execroot entry point"),
-rather than the copy inside the runfiles symlink tree. With everything
-consolidated in `bazel-out/<target-cfg>/bin/`, Node.js sees a single
+The entry point used is the one in the target-platform bin
+directory--confusingly called the "execroot entry point" even though the
+execroot encompasses the whole sandbox. In order for the tool's sources to land
+in that directory, they end up being rebuilt for the target platform. With
+everything consolidated in `bazel-out/<target-cfg>/bin/`, Node.js sees a single
`node_modules` tree. This can be the right choice for some frameworks such as
Next.js, which expects inputs and outputs to be in the same directory tree.
@@ -46,17 +51,17 @@
issues. However, you must ensure that any code executed during the build (for
example, JavaScript config files for tools like Webpack or Rspack) is a declared
dependency of the `js_binary` tool, not merely a source file passed to
-`js_run_binary`. Config files in the `js_run_binary`'s `srcs` will land in the
+`js_run_binary`. Source files in the `js_run_binary`'s `srcs` will land in the
target-platform bin directory and will therefore not be visible to the tool's
runfiles resolution.
## Recommendation
-We recommend setting `use_execroot_entry_point = False` wherever possible and
-ensuring that all code executed during the build is declared as a dependency of
-the `js_binary`. The main exception is Next.js and similar frameworks that
-expect inputs and outputs in the same directory tree or that execute
-target-platform code during the build, in which case `True` is required.
+We recommend setting `use_execroot_entry_point = False`, unless your tool
+requires its config and outputs to be adjacent to each other in the same
+directory (such as Next.js for example). If you do this and ensure that all
+code executed during the build is declared as a dependency of the `js_binary`,
+then your build will work reliably even in cross-platform situations.
To disable `use_execroot_entry_point` by default, pass the build flag:
@@ -64,8 +69,69 @@
--@aspect_rules_js//js:use_execroot_entry_point=False
```
+You may want to set it in your `.bazelrc` as follows:
+```
+common --@aspect_rules_js//js:use_execroot_entry_point=False
+```
+
Individual targets can still override the flag by explicitly setting
`use_execroot_entry_point = True` or `use_execroot_entry_point = False`.
In a future major version, we will likely disable the `use_execroot_entry_point`
behavior by default.
+
+### Example
+
+A simple demonstration of the recommended way to set up a `js_run_binary`
+target is the [Rspack example](../examples/rspack/BUILD.bazel). This particular
+case *requires* `use_execroot_entry_point = False`, because otherwise the
+cross-platform build in that file would fail as a result of Bazel trying to use
+the wrong Rspack binary. Below is the key part, edited slightly for brevity:
+
+```
+js_library(
+ name = "rspack_config",
+ srcs = ["rspack.config.cjs"],
+ deps = [":node_modules/@rspack/cli"],
+)
+
+bin.rspack(
+ name = "rspack_build",
+ srcs = ["rspack_entry.js"],
+ outs = ["rspack/main.bundle.js"],
+ chdir = package_name(),
+ data = [":rspack_config"],
+ fixed_args = [
+ "build",
+ "--config",
+ "$$RUNFILES_DIR/$(rlocationpath :rspack_config)",
+ ],
+ use_execroot_entry_point = False,
+)
+```
+
+Note that in this case `bin.rspack()` is a generated macro that creates both
+the `js_binary` for Rspack *and* the `js_run_binary` target that runs it.
+
+Key points:
+- The `rspack.config.cjs` file is wrapped in a `js_library` and taken as a
+ `data` dependency of the `js_binary`. This ensures that the config file and
+ its dependencies are built for the exec platform, which is appropriate
+ since they will run during the build action. They will land in the runfiles
+ directory adjacent to the other exec-platform sources, which will allow
+ module resolution to proceed correctly.
+- `chdir = package_name()` causes the working directory to be
+ `bazel-out/<target-cfg>/bin/rspack`. This is not strictly necessary, but it
+ is convenient to have the outputs go directly in the build action's current
+ directory.
+- The config file (`rspack.config.cjs`) refers to `process.cwd()`, not
+ `__dirname`, for specifying the output path. This is key, because the config
+ file (and therefore `__dirname`) will be in the runfiles directory and not
+ the output tree.
+- We refer to `"$$RUNFILES_DIR/$(rlocationpath :rspack_config)"` in
+ `fixed_args`. `$(rlocationpath ...)` is evaluated at analysis time and
+ determines the path to the config file within the runfiles directory. This
+ argument must go in `fixed_args` rather than `args`, to allow `$RUNFILES_DIR`
+ to be evaluated at run time. Note the double dollar sign (`$$`) to prevent
+ the `js_binary` implementation from attempting to evaluate that variable at
+ analysis time.