Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ npm/private/test/subs/package.json=-897547334
npm/private/test/vendored/is-odd/package.json=1041695223
npm/private/test/vendored/lodash-4.17.21.tgz=-1206623349
npm/private/test/vendored/semver-max/package.json=578664053
package.json=1856600154
pnpm-lock.yaml=-923227665
package.json=564790136
pnpm-lock.yaml=-1392643421
pnpm-workspace.yaml=1041196754
21 changes: 20 additions & 1 deletion .aspect/workflows/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ workspaces:
without: true
- bazel-9:
without: true
- bazel-9-no-execroot-entry-point:
without: true
e2e/pnpm_lockfiles:
icon: pnpm
tasks: *e2e_tasks
Expand All @@ -117,7 +119,17 @@ workspaces:
tasks: *e2e_tasks
e2e/repo_mapping:
icon: js
tasks: *e2e_tasks
tasks:
- test:
queue: aspect-medium
- format:
without: true
- buildifier:
without: true
# aspect_rules_js is given a different name in this repo, so we
# cannot use a flag beginning with --@aspect_rules_js//.
- bazel-9-no-execroot-entry-point:
without: true
e2e/output_paths:
icon: js
tasks: *e2e_tasks
Expand Down Expand Up @@ -167,6 +179,13 @@ tasks:
# TODO: change this back to 9.x once this bug is fixed:
# https://github.com/bazelbuild/bazel/issues/29393
USE_BAZEL_VERSION: '9.0.2'
- test:
name: Bazel 9 (no execroot entry point)
id: bazel-9-no-execroot-entry-point
bazel:
flags: ['--test_tag_filters=-skip-on-bazel9', '--@aspect_rules_js//js:use_execroot_entry_point=False']
env:
USE_BAZEL_VERSION: '9.0.2'
- format:
queue: aspect-medium
- buildifier:
Expand Down
8 changes: 6 additions & 2 deletions contrib/nextjs/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def nextjs(
**kwargs
)

def nextjs_build(name, config, srcs, next_js_binary, data = [], **kwargs):
def nextjs_build(name, config, srcs, next_js_binary, data = [], use_execroot_entry_point = True, **kwargs):
"""Build the Next.js production artifact.

See https://nextjs.org/docs/pages/api-reference/cli/next#build
Expand All @@ -191,6 +191,7 @@ def nextjs_build(name, config, srcs, next_js_binary, data = [], **kwargs):

See main docstring above for example usage.

use_execroot_entry_point: passed directly to js_run_binary
**kwargs: Other attributes passed to all targets such as `tags`, env
"""
js_run_binary(
Expand All @@ -202,6 +203,7 @@ def nextjs_build(name, config, srcs, next_js_binary, data = [], **kwargs):
chdir = native.package_name(),
mnemonic = "NextJs",
progress_message = "Compile Next.js app %{label}",
use_execroot_entry_point = use_execroot_entry_point,
**kwargs
)

Expand Down Expand Up @@ -268,7 +270,7 @@ def nextjs_dev(name, config, srcs, data, next_js_binary, **kwargs):
# Standalone Next.js build & server binary.
# ---------------------------------------------------------------------------------------------

def nextjs_standalone_build(name, config, srcs, next_js_binary, data = [], **kwargs):
def nextjs_standalone_build(name, config, srcs, next_js_binary, data = [], use_execroot_entry_point = True, **kwargs):
"""Compile a standalone Next.js application.

See https://nextjs.org/docs/app/api-reference/config/next-config-js/output#automatically-copying-traced-files
Expand All @@ -290,6 +292,7 @@ def nextjs_standalone_build(name, config, srcs, next_js_binary, data = [], **kwa
srcs: the sources to include in the build, including any transitive deps
next_js_binary: the Next.js binary to use for building
data: the data files to include in the build
use_execroot_entry_point: passed directly to js_run_binary
**kwargs: Other attributes passed to all targets such as `tags`, env
"""

Expand Down Expand Up @@ -327,6 +330,7 @@ def nextjs_standalone_build(name, config, srcs, next_js_binary, data = [], **kwa
chdir = native.package_name(),
mnemonic = "NextJs",
progress_message = "Compile Next.js standalone app %{label}",
use_execroot_entry_point = use_execroot_entry_point,
**kwargs
)

Expand Down
71 changes: 71 additions & 0 deletions docs/use_execroot_entry_point.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# use_execroot_entry_point
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in a followup PR, but currently nothing actually links to this doc? Maybe #2852 needs to solve that as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, nothing links to this yet. Maybe in a followup I should bring back docs/README.md and point to the new file as part of fixing #2852?


This page describes the `use_execroot_entry_point` option on `js_run_binary`
and provides guidance on when to use each value.

## 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:

- **Exec-platform bin** (`bazel-out/<exec-cfg>/bin/`): where build artifacts for
the exec platform land.
- **Runfiles tree** (`bazel-out/<exec-cfg>/bin/path/to/my_binary.runfiles/`):
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.

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.

## 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
`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.

The tradeoff is that if the exec platform differs from the target platform (for
example, cross-compiling from macOS to Linux), target-platform artifacts such as
native Node.js addons are rebuilt for the target and may fail to run on the exec
platform.

**`use_execroot_entry_point = False`:**
The entry point used is the one from the runfiles tree. All code executed during
the build action runs from the runfiles tree, which avoids cross-platform
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
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.

To disable `use_execroot_entry_point` by default, pass the build flag:

```
--@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.
14 changes: 14 additions & 0 deletions js/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
"Public API"

load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")

# This flag controls the default behavior of the use_execroot_entry_point flag
# on js_run_binary.
bool_flag(
name = "use_execroot_entry_point",
build_setting_default = True,
visibility = ["//visibility:public"],
)

config_setting(
name = "_use_execroot_entry_point_true",
flag_values = {"use_execroot_entry_point": "True"},
)

bzl_library(
name = "defs",
Expand Down
45 changes: 31 additions & 14 deletions js/private/js_run_binary.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def js_run_binary(
stderr = None,
exit_code_out = None,
silent_on_success = True,
use_execroot_entry_point = True,
use_execroot_entry_point = None,
copy_srcs_to_bin = True,
include_sources = True,
include_types = False,
Expand Down Expand Up @@ -137,18 +137,18 @@ def js_run_binary(
use_execroot_entry_point: Use the `entry_point` script of the `js_binary` `tool` that is in the execroot output tree
instead of the copy that is in runfiles.

If `None` (the default), the behavior is controlled by the `//js:use_execroot_entry_point` build flag,
which defaults to `True`.

Runfiles of `tool` are all hoisted to `srcs` of the underlying `run_binary` so they are included as execroot
inputs to the action.

Using the entry point script that is in the execroot output tree means that there will be no conflicting
runfiles `node_modules` in the node_modules resolution path which can confuse npm packages such as next and
react that don't like being resolved in multiple node_modules trees. This more closely emulates the
environment that tools such as Next.js see when they are run outside of Bazel.

When True, the `js_binary` tool must have `copy_data_to_bin` set to True (the default) so that all data files
When enabled, the `js_binary` tool must have `copy_data_to_bin` set to True (the default) so that all data files
needed by the binary are available in the execroot output tree. This requirement can be turned off with by
setting `allow_execroot_entry_point_with_no_copy_data_to_bin` to True.

See docs/use_execroot_entry_point.md for more information.

copy_srcs_to_bin: When True, all srcs files are copied to the output tree that are not already there.

include_sources: see `js_info_files` documentation
Expand Down Expand Up @@ -356,10 +356,10 @@ See https://github.com/aspect-build/rules_js/tree/main/docs#using-binaries-publi
name = bazel_lib_utils.to_label(name),
))

# Configure run from execroot
if use_execroot_entry_point:
fixed_env["JS_BINARY__USE_EXECROOT_ENTRY_POINT"] = "1"

# Configure run from execroot.
# When use_execroot_entry_point is None, behavior is controlled by the //js:use_execroot_entry_point flag.
execroot_extra_srcs = []
if use_execroot_entry_point != False:
# hoist all runfiles to srcs when running from execroot
js_runfiles_lib_name = "{}_runfiles_lib".format(name)
_js_library(
Expand All @@ -380,16 +380,33 @@ See https://github.com/aspect-build/rules_js/tree/main/docs#using-binaries-publi
# Always propagate the testonly attribute
testonly = kwargs.get("testonly", False),
)
extra_srcs.append(":{}".format(js_runfiles_name))

if use_execroot_entry_point == True:
fixed_env["JS_BINARY__USE_EXECROOT_ENTRY_POINT"] = "1"
execroot_extra_srcs = [":{}".format(js_runfiles_name)]
else:
# None: use the flag to decide at analysis time
execroot_extra_srcs = select({
Label("//js:_use_execroot_entry_point_true"): [":{}".format(js_runfiles_name)],
"//conditions:default": [],
})

if allow_execroot_entry_point_with_no_copy_data_to_bin:
fixed_env["JS_BINARY__ALLOW_EXECROOT_ENTRY_POINT_WITH_NO_COPY_DATA_TO_BIN"] = "1"

# When use_execroot_entry_point is None, the env var is resolved at analysis time via the flag.
# We use a select() only for the conditional portion and merge with | outside of it, so that
# a user-supplied env that is itself a select() remains valid on the right-hand side.
execroot_env = select({
Label("//js:_use_execroot_entry_point_true"): {"JS_BINARY__USE_EXECROOT_ENTRY_POINT": "1"},
"//conditions:default": {},
}) if use_execroot_entry_point == None else {}

_run_binary(
name = name,
tool = tool,
env = fixed_env | env,
srcs = srcs + extra_srcs,
env = fixed_env | execroot_env | env,
srcs = srcs + extra_srcs + execroot_extra_srcs,
outs = outs + extra_outs,
out_dirs = out_dirs,
args = args,
Expand Down
5 changes: 5 additions & 0 deletions js/private/test/data/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ js_test(
# TEST: js_test(data = js_run_binary(srcs)) ---------------
js_run_binary(
name = "run-write-srcs",
srcs = ["data.json"],
outs = ["rb0.json"],
args = [
"rb0.json",
Expand All @@ -178,6 +179,7 @@ js_test(
# TEST: js_test(data = js_run_binary(srcs = js_library(srcs))) ----------------
js_run_binary(
name = "run-write-js_library-srcs",
srcs = ["data.json"],
outs = ["rb1.json"],
args = [
"rb1.json",
Expand Down Expand Up @@ -210,6 +212,7 @@ js_binary(

js_run_binary(
name = "run-write-js_library-data",
srcs = ["data.json"],
outs = ["rb2.json"],
args = [
"rb2.json",
Expand All @@ -235,6 +238,7 @@ js_test(
# TEST: js_test(data = js_run_binary(srcs = genrule())) -----------------------
js_run_binary(
name = "run-write-generated",
srcs = ["data-generated.json"],
outs = ["rb3.json"],
args = [
"rb3.json",
Expand All @@ -260,6 +264,7 @@ js_test(
# TEST: js_test(data = js_run_binary(srcs = genrule(srcs = filegroup))) -------
js_run_binary(
name = "run-write-generated-copied",
srcs = ["data-copied.json"],
outs = ["rb4.json"],
args = [
"rb4.json",
Expand Down
Loading
Loading