From f75372d386bc9c6f9ac3242daa1c5bda0023faf0 Mon Sep 17 00:00:00 2001 From: Adam Cozzette Date: Fri, 14 Aug 2026 16:23:29 -0700 Subject: [PATCH 1/2] refactor: use hermetic_launcher to invoke node We currently have a trivial shell script that invokes node with a `--require` flag for our patches. This change replaces that with a hermetic_launcher. There is no one global node binary we can use, since each toolchain can configure its own node. As a result, we need to stamp out a hermetic_launcher for each js_binary target. This is actually nice, though, because this will allow us to incrementally migrate away from our bash launcher and toward hermetic_launcher. With this change, we have a bash launcher wrapping a hermetic_launcher which itself wraps node. hermetic_launcher does not support linux ppc64le or windows arm64, so js_binary no longer builds for those two platforms. If we get complaints, we can look into adding support upstream. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci-workflows.yaml | 1 - MODULE.bazel | 1 + e2e/no_shell_toolchain/BUILD.bazel | 13 +++ e2e/no_shell_toolchain/MODULE.bazel | 2 + js/defs.bzl | 2 +- js/private/BUILD.bazel | 3 +- js/private/js_binary.bzl | 98 ++++++++++++++++--- js/private/js_binary.sh.tpl | 9 +- js/private/node_bin/node | 5 - js/private/node_bin_windows/node.bat | 3 - js/private/test/image/asserts.bzl | 11 ++- .../test/image/custom_owner_test_app.listing | 4 +- .../test/image/default_test_app.listing | 4 +- .../custom_layer_groups_test_app.listing | 4 +- .../rspack_linux_arm64_test_app.listing | 4 +- .../image/regex_edge_cases_test_app.listing | 4 +- js/private/test/snapshots/launcher.sh | 9 +- 17 files changed, 138 insertions(+), 39 deletions(-) delete mode 100755 js/private/node_bin/node delete mode 100755 js/private/node_bin_windows/node.bat diff --git a/.github/workflows/ci-workflows.yaml b/.github/workflows/ci-workflows.yaml index 17ba61720a..d0a389cf12 100644 --- a/.github/workflows/ci-workflows.yaml +++ b/.github/workflows/ci-workflows.yaml @@ -272,7 +272,6 @@ jobs: # https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#included-storage-and-minutes smoke: name: smoke (${{ matrix.workspace.path }}, ${{ matrix.os }}) - if: github.ref_name == 'main' || contains(github.head_ref, 'macos') || contains(github.head_ref, 'windows') runs-on: ${{ matrix.os }} strategy: fail-fast: false diff --git a/MODULE.bazel b/MODULE.bazel index 16e891975b..9f2320a90f 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -16,6 +16,7 @@ bazel_dep(name = "bazel_features", version = "1.41.0") bazel_dep(name = "bazel_skylib", version = "1.5.0") bazel_dep(name = "platforms", version = "1.0.0") bazel_dep(name = "rules_nodejs", version = "6.7.3") +bazel_dep(name = "hermetic_launcher", version = "0.0.15") # Changes ensured by rules_js: # 3.2.2: https://github.com/bazel-contrib/bazel-lib/commit/cac2d7855949d1b222fa26888892fbbe1d31015d diff --git a/e2e/no_shell_toolchain/BUILD.bazel b/e2e/no_shell_toolchain/BUILD.bazel index 370f034301..8e93252255 100644 --- a/e2e/no_shell_toolchain/BUILD.bazel +++ b/e2e/no_shell_toolchain/BUILD.bazel @@ -46,3 +46,16 @@ toolchain( toolchain = "@nodejs_linux_amd64//:toolchain", toolchain_type = "@rules_nodejs//nodejs:runtime_toolchain_type", ) + +# Likewise for the launcher template that js_binary stamps its node launcher from. +# hermetic_launcher ships no iOS stub; as with the Node.js runtime above, any +# platform's works here since the js_binary is only built, never run. +toolchain( + name = "launcher_template_ios_arm64_toolchain", + target_compatible_with = [ + "@platforms//os:ios", + "@platforms//cpu:arm64", + ], + toolchain = "@hermetic_launcher//launcher/private/prebuilt:template_x86_64_linux_prebuilt", + toolchain_type = "@hermetic_launcher//launcher:template_toolchain_type", +) diff --git a/e2e/no_shell_toolchain/MODULE.bazel b/e2e/no_shell_toolchain/MODULE.bazel index b3e7bf8d53..5da90a88a3 100644 --- a/e2e/no_shell_toolchain/MODULE.bazel +++ b/e2e/no_shell_toolchain/MODULE.bazel @@ -4,6 +4,7 @@ bazel_dep(name = "aspect_rules_js", version = "0.0.0", dev_dependency = True) bazel_dep(name = "bazel_skylib", version = "1.8.1", dev_dependency = True) bazel_dep(name = "platforms", version = "0.0.11", dev_dependency = True) bazel_dep(name = "rules_nodejs", version = "6.6.0", dev_dependency = True) +bazel_dep(name = "hermetic_launcher", version = "0.0.15", dev_dependency = True) local_path_override( module_name = "aspect_rules_js", @@ -18,6 +19,7 @@ use_repo(node, "nodejs_linux_amd64") register_toolchains( "//:nodejs_ios_arm64_toolchain", + "//:launcher_template_ios_arm64_toolchain", dev_dependency = True, ) diff --git a/js/defs.bzl b/js/defs.bzl index 6add5fd781..9a1a4dbe50 100644 --- a/js/defs.bzl +++ b/js/defs.bzl @@ -76,7 +76,7 @@ def js_binary(**kwargs): * JS_BINARY__NODE_BINARY: the Node.js binary path run by the `js_binary` target * JS_BINARY__NPM_BINARY: the npm binary path; this is available when `include_npm` is `True` on the `js_binary` target - * JS_BINARY__NODE_WRAPPER: the Node.js wrapper script used to run Node.js which is available as `node` on the `PATH` at runtime + * JS_BINARY__NODE_WRAPPER: the Node.js launcher binary used to run Node.js which is available as `node` on the `PATH` at runtime * JS_BINARY__RUNFILES: the absolute path to the Bazel runfiles directory * JS_BINARY__EXECROOT: the absolute path to the root of the execution root for the action; if in the sandbox, this path absolute path to the root of the execution root within the sandbox diff --git a/js/private/BUILD.bazel b/js/private/BUILD.bazel index 42ed04c3d7..e4cd4ff68a 100644 --- a/js/private/BUILD.bazel +++ b/js/private/BUILD.bazel @@ -6,8 +6,6 @@ package(default_visibility = ["//visibility:public"]) exports_files([ "js_binary.sh.tpl", - "node_bin/node", - "node_bin_windows/node.bat", "npm_bin/npm", "npm_bin_windows/npm.bat", "js_image_layer.mjs", @@ -50,6 +48,7 @@ bzl_library( "@bazel_lib//lib:windows_utils", "@bazel_skylib//lib:dicts", "@bazel_tools//tools/build_defs/repo:cache.bzl", + "@hermetic_launcher//launcher:lib_bzl", ], ) diff --git a/js/private/js_binary.bzl b/js/private/js_binary.bzl index e25c01f296..77527b7008 100644 --- a/js/private/js_binary.bzl +++ b/js/private/js_binary.bzl @@ -4,6 +4,7 @@ load("@bazel_lib//lib:copy_to_bin.bzl", "COPY_FILE_TO_BIN_TOOLCHAINS") load("@bazel_lib//lib:directory_path.bzl", "DirectoryPathInfo") load("@bazel_lib//lib:expand_make_vars.bzl", "expand_locations", "expand_variables") load("@bazel_lib//lib:windows_utils.bzl", "create_windows_native_launcher_script") +load("@hermetic_launcher//launcher:lib.bzl", "launcher") load(":bash.bzl", "BASH_INITIALIZE_RUNFILES") load(":js_helpers.bzl", "LOG_LEVELS", "envs_for_log_level", "gather_files_from_js_infos", "gather_runfiles") @@ -249,17 +250,8 @@ _ATTRS = { default = Label("//js/private:js_binary.sh.tpl"), allow_single_file = True, ), - # Windows gets its own separate directory for node and npm wrappers. This - # ensures that the bash scripts do not end up on the PATH when we build for - # Windows. - "_node_wrapper_sh": attr.label( - default = Label("//js/private:node_bin/node"), - allow_single_file = True, - ), - "_node_wrapper_bat": attr.label( - default = Label("//js/private:node_bin_windows/node.bat"), - allow_single_file = True, - ), + # Windows gets its own separate directory for the npm wrapper. This ensures + # that the bash script does not end up on the PATH when we build for Windows. "_npm_wrapper_sh": attr.label( default = Label("//js/private:npm_bin/npm"), allow_single_file = True, @@ -297,6 +289,78 @@ def _generates_coverage_report(ctx): ctx.attr.testonly and ctx.configuration.coverage_enabled) +# Resolved here as Labels rather than used as the bare strings hermetic_launcher +# exposes: under --incompatible_auto_exec_groups a string toolchain type is resolved +# against the repository mapping of whichever module is being built, so a consumer +# that does not itself depend on hermetic_launcher cannot resolve the name. A Label +# is resolved against this file's own mapping at load time instead. +_FINALIZER_TOOLCHAIN_TYPE = Label(launcher.finalizer_toolchain_type) +_TEMPLATE_TOOLCHAIN_TYPE = Label(launcher.template_toolchain_type) + +def _compile_stub(ctx, embedded_args, transformed_args, output_file): + """Stamps a launcher binary from the prebuilt template stub. + + This is `launcher.compile_stub` reimplemented so the finalizer toolchain can be + named by Label; see the comment on _FINALIZER_TOOLCHAIN_TYPE. Drop this in favour + of the upstream helper once it takes Labels. + """ + template = ctx.toolchains[_TEMPLATE_TOOLCHAIN_TYPE].templatetoolchaininfo.template_exe + args = ctx.actions.args() + args.add("--template", template) + args.add("-o", output_file) + args.add_joined("--transform", transformed_args, join_with = ",") + args.add("--") + args.add_all(embedded_args) + ctx.actions.run( + outputs = [output_file], + executable = ctx.toolchains[_FINALIZER_TOOLCHAIN_TYPE].finalizer_info.finalizer, + arguments = [args], + inputs = [template], + toolchain = _FINALIZER_TOOLCHAIN_TYPE, + mnemonic = "JsLauncher", + progress_message = "Stamping launcher %{output}", + ) + +def _rlocation_path(ctx, file): + """The runfiles-root-relative path of a file, as the launcher binaries resolve it.""" + if file.short_path.startswith("../"): + return file.short_path[3:] + return "{}/{}".format(ctx.workspace_name, file.short_path) + +def _compile_node_launcher(ctx, nodeinfo, output, args): + """Compiles a launcher binary that runs node with `args` prepended to its argv. + + Each entry of `args` is either a File, which is resolved through runfiles when the + launcher runs, or a string, which is embedded verbatim. + """ + if nodeinfo.node: + embedded_args, transformed_args = launcher.args_from_entrypoint(nodeinfo.node) + elif nodeinfo.node_path.startswith("/"): + # A node_toolchain may name a non-hermetic node by absolute path rather than + # provide a File. The launcher passes absolute paths through untouched, so + # there is nothing for it to resolve. + embedded_args, transformed_args = [nodeinfo.node_path], [] + else: + # A relative node_path is relative to this workspace within the runfiles tree, + # matching how the launcher script resolves it. + embedded_args, transformed_args = ["{}/{}".format(ctx.workspace_name, nodeinfo.node_path)], [0] + + for arg in args: + if type(arg) == "File": + embedded_args, transformed_args = launcher.append_runfile( + file = arg, + embedded_args = embedded_args, + transformed_args = transformed_args, + ) + else: + embedded_args, transformed_args = launcher.append_embedded_arg( + arg = arg, + embedded_args = embedded_args, + transformed_args = transformed_args, + ) + + _compile_stub(ctx, embedded_args, transformed_args, output) + def _bash_launcher(ctx, nodeinfo, entry_point_path, log_prefix_rule_set, log_prefix_rule, fixed_args, fixed_env, is_windows): # Explicitly disable node fs patches on Windows: # https://github.com/aspect-build/rules_js/issues/1137 @@ -390,7 +454,14 @@ def _bash_launcher(ctx, nodeinfo, entry_point_path, log_prefix_rule_set, log_pre if ctx.attr.expand_args: fixed_args = [expand_variables(ctx, expand_locations(ctx, fixed_arg, ctx.attr.data)) for fixed_arg in fixed_args] - node_wrapper = ctx.file._node_wrapper_bat if is_windows else ctx.file._node_wrapper_sh + # Windows needs the .exe suffix for this to be found by a bare `node` on the PATH. + # The launcher script puts the parent directory on the PATH, so the launcher gets a + # directory to itself and cannot shadow the npm wrapper sitting next to it. + node_wrapper = ctx.actions.declare_file("{}_/node_bin/node{}".format( + ctx.label.name, + ".exe" if is_windows else "", + )) + _compile_node_launcher(ctx, nodeinfo, node_wrapper, ["--require", ctx.file._node_patches]) toolchain_files = [node_wrapper] npm_path = "" @@ -421,6 +492,7 @@ def _bash_launcher(ctx, nodeinfo, entry_point_path, log_prefix_rule_set, log_pre "{{log_prefix_rule}}": log_prefix_rule, "{{node_options}}": "\n".join(node_options), "{{node_patches}}": ctx.file._node_patches.short_path, + "{{node_patches_rlocation}}": _rlocation_path(ctx, ctx.file._node_patches), "{{node_wrapper}}": node_wrapper.short_path, "{{node}}": node_path, "{{npm}}": npm_path, @@ -625,6 +697,8 @@ js_binary_lib = struct( # Optional: only referenced on Windows config_common.toolchain_type("@bazel_tools//tools/sh:toolchain_type", mandatory = False), "@rules_nodejs//nodejs:runtime_toolchain_type", + _FINALIZER_TOOLCHAIN_TYPE, + _TEMPLATE_TOOLCHAIN_TYPE, ] + COPY_FILE_TO_BIN_TOOLCHAINS, ) diff --git a/js/private/js_binary.sh.tpl b/js/private/js_binary.sh.tpl index b21bc2e3c0..5fe8518766 100644 --- a/js/private/js_binary.sh.tpl +++ b/js/private/js_binary.sh.tpl @@ -336,7 +336,7 @@ fi if [ "${JS_BINARY__NO_RUNFILES:-}" ]; then export JS_BINARY__NODE_WRAPPER - JS_BINARY__NODE_WRAPPER=$(resolve_execroot_src_path "{{node_wrapper}}") + JS_BINARY__NODE_WRAPPER=$(resolve_execroot_bin_path "{{node_wrapper}}") else export JS_BINARY__NODE_WRAPPER="$JS_BINARY__RUNFILES/{{workspace_name}}/{{node_wrapper}}" fi @@ -353,7 +353,12 @@ if [ "${JS_BINARY__NO_RUNFILES:-}" ]; then export JS_BINARY__NODE_PATCHES JS_BINARY__NODE_PATCHES=$(resolve_execroot_src_path "{{node_patches}}") else - export JS_BINARY__NODE_PATCHES="$JS_BINARY__RUNFILES/{{workspace_name}}/{{node_patches}}" + # Spelled as a runfiles-root-relative path rather than via + # "{{workspace_name}}/../", so that this is byte-for-byte the path the node + # launcher binary passes to --require. A child process inherits this one in + # execArgv and gets the launcher's as well; if the two spellings differ, node + # loads the patches twice and the second application fails. + export JS_BINARY__NODE_PATCHES="$JS_BINARY__RUNFILES/{{node_patches_rlocation}}" fi if [ ! -f "$JS_BINARY__NODE_PATCHES" ]; then logf_fatal "node patches '%s' not found" "$JS_BINARY__NODE_PATCHES" diff --git a/js/private/node_bin/node b/js/private/node_bin/node deleted file mode 100755 index fae2c2a56f..0000000000 --- a/js/private/node_bin/node +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -o pipefail -o errexit -o nounset - -exec "$JS_BINARY__NODE_BINARY" --require "$JS_BINARY__NODE_PATCHES" "$@" diff --git a/js/private/node_bin_windows/node.bat b/js/private/node_bin_windows/node.bat deleted file mode 100755 index 85ad5d2bf7..0000000000 --- a/js/private/node_bin_windows/node.bat +++ /dev/null @@ -1,3 +0,0 @@ -@if not defined DEBUG_HELPER @ECHO OFF - -%JS_BINARY__NODE_BINARY% --require %JS_BINARY__NODE_PATCHES% %* diff --git a/js/private/test/image/asserts.bzl b/js/private/test/image/asserts.bzl index de5fe49c1e..4b8d222f2d 100644 --- a/js/private/test/image/asserts.bzl +++ b/js/private/test/image/asserts.bzl @@ -13,12 +13,21 @@ SKIP_ON_WINDOWS = select({ # js_binary launcher scripts have unstable sizes across Bazel versions. _UNSTABLE_SIZE_BASENAMES = ["bin", "bin2"] +# The node launcher binary is stamped from a prebuilt hermetic_launcher stub, so its +# size moves with the stub's platform and version. Matched by path rather than +# basename so the real node binary's size stays pinned. +_UNSTABLE_SIZE_PATHS = ["node_bin/node"] + # buildifier: disable=function-docstring def assert_tar_listing(name, actual, expected): launcher_alt = "|".join(_UNSTABLE_SIZE_BASENAMES) + stub_alt = "|".join([p.replace("/", "\\/") for p in _UNSTABLE_SIZE_PATHS]) # `$$` escapes `$` for Bazel genrule cmd Make-variable expansion. - size_sanitize = "sed -E '/\\/({})$$/ s/[0-9]+ Jan/xxxxx Jan/'".format(launcher_alt) + size_sanitize = ( + "sed -E -e '/\\/({})$$/ s/[0-9]+ Jan/xxxxx Jan/'".format(launcher_alt) + + " -e '/_\\/({})$$/ s/[0-9]+ Jan/xxxxx Jan/'".format(stub_alt) + ) # Normalize bzlmod canonical repo separators: ~~/~ (Bazel 7) -> ++/+ (Bazel 8+). repo_sep_normalize = ( diff --git a/js/private/test/image/custom_owner_test_app.listing b/js/private/test/image/custom_owner_test_app.listing index 586923d893..ff8cf3a3d3 100644 --- a/js/private/test/image/custom_owner_test_app.listing +++ b/js/private/test/image/custom_owner_test_app.listing @@ -7,8 +7,6 @@ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runf drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/ -drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/node_bin/ --r-xr-xr-x 0 100 0 133 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/node_bin/node drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image-fixture-a/ @@ -17,4 +15,6 @@ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runf -r-xr-xr-x 0 100 0 168 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image-fixture-d/package.json drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/ -r-xr-xr-x 0 100 0 xxxxx Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/bin +drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/node_bin/ +-r-xr-xr-x 0 100 0 xxxxx Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/node_bin/node -r-xr-xr-x 0 100 0 20 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/main.js diff --git a/js/private/test/image/default_test_app.listing b/js/private/test/image/default_test_app.listing index 917b36bb25..59e5cfe748 100644 --- a/js/private/test/image/default_test_app.listing +++ b/js/private/test/image/default_test_app.listing @@ -7,8 +7,6 @@ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runf drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/ -drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/node_bin/ --r-xr-xr-x 0 0 0 133 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/node_bin/node drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image-fixture-a/ @@ -17,4 +15,6 @@ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runf -r-xr-xr-x 0 0 0 168 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image-fixture-d/package.json drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/ -r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/bin +drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/node_bin/ +-r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/node_bin/node -r-xr-xr-x 0 0 0 20 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/main.js diff --git a/js/private/test/image/non_ascii/custom_layer_groups_test_app.listing b/js/private/test/image/non_ascii/custom_layer_groups_test_app.listing index ec53185d64..c9a459d00e 100644 --- a/js/private/test/image/non_ascii/custom_layer_groups_test_app.listing +++ b/js/private/test/image/non_ascii/custom_layer_groups_test_app.listing @@ -9,13 +9,13 @@ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/ -drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/node_bin/ --r-xr-xr-x 0 0 0 133 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/node_bin/node drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/ -r-xr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/ㅑㅕㅣㅇ.ㄴㅅ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/bin2_/ -r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/bin2_/bin2 +drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/bin2_/node_bin/ +-r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/bin2_/node_bin/node -r-xr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/empty empty.ㄴㅅ -r-xr-xr-x 0 0 0 20 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/main.js diff --git a/js/private/test/image/platform_deps/rspack_linux_arm64_test_app.listing b/js/private/test/image/platform_deps/rspack_linux_arm64_test_app.listing index dbbada3c00..6cd9488b92 100644 --- a/js/private/test/image/platform_deps/rspack_linux_arm64_test_app.listing +++ b/js/private/test/image/platform_deps/rspack_linux_arm64_test_app.listing @@ -9,11 +9,11 @@ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/plat drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/ -drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/node_bin/ --r-xr-xr-x 0 0 0 133 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/node_bin/node drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/test/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/test/image/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/test/image/platform_deps/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/test/image/platform_deps/bin_/ -r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/test/image/platform_deps/bin_/bin +drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/test/image/platform_deps/bin_/node_bin/ +-r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/test/image/platform_deps/bin_/node_bin/node -r-xr-xr-x 0 0 0 20 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/test/image/platform_deps/main.js diff --git a/js/private/test/image/regex_edge_cases_test_app.listing b/js/private/test/image/regex_edge_cases_test_app.listing index e3d8f194fe..52196f5638 100644 --- a/js/private/test/image/regex_edge_cases_test_app.listing +++ b/js/private/test/image/regex_edge_cases_test_app.listing @@ -8,8 +8,6 @@ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin. drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/ -drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/node_bin/ --r-xr-xr-x 0 0 0 133 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/node_bin/node drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image-fixture-a/ @@ -18,4 +16,6 @@ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin. -r-xr-xr-x 0 0 0 168 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image-fixture-d/package.json drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/ -r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/bin +drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/node_bin/ +-r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/node_bin/node -r-xr-xr-x 0 0 0 20 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/main.js diff --git a/js/private/test/snapshots/launcher.sh b/js/private/test/snapshots/launcher.sh index 8627165643..a4d1687b77 100644 --- a/js/private/test/snapshots/launcher.sh +++ b/js/private/test/snapshots/launcher.sh @@ -456,9 +456,9 @@ fi if [ "${JS_BINARY__NO_RUNFILES:-}" ]; then export JS_BINARY__NODE_WRAPPER - JS_BINARY__NODE_WRAPPER=$(resolve_execroot_src_path "js/private/node_bin/node") + JS_BINARY__NODE_WRAPPER=$(resolve_execroot_bin_path "js/private/test/shellcheck_launcher_/node_bin/node") else - export JS_BINARY__NODE_WRAPPER="$JS_BINARY__RUNFILES/_main/js/private/node_bin/node" + export JS_BINARY__NODE_WRAPPER="$JS_BINARY__RUNFILES/_main/js/private/test/shellcheck_launcher_/node_bin/node" fi if [ ! -f "$JS_BINARY__NODE_WRAPPER" ]; then logf_fatal "node wrapper '%s' not found" "$JS_BINARY__NODE_WRAPPER" @@ -473,6 +473,11 @@ if [ "${JS_BINARY__NO_RUNFILES:-}" ]; then export JS_BINARY__NODE_PATCHES JS_BINARY__NODE_PATCHES=$(resolve_execroot_src_path "js/private/node-bootstrap/bootstrap.cjs") else + # Spelled as a runfiles-root-relative path rather than via + # "_main/../", so that this is byte-for-byte the path the node + # launcher binary passes to --require. A child process inherits this one in + # execArgv and gets the launcher's as well; if the two spellings differ, node + # loads the patches twice and the second application fails. export JS_BINARY__NODE_PATCHES="$JS_BINARY__RUNFILES/_main/js/private/node-bootstrap/bootstrap.cjs" fi if [ ! -f "$JS_BINARY__NODE_PATCHES" ]; then From ed3ac75b2e336eec9862caf8004ed1659641b8a1 Mon Sep 17 00:00:00 2001 From: Adam Cozzette Date: Mon, 17 Aug 2026 09:14:26 -0700 Subject: [PATCH 2/2] refactor: make the bash launcher independent of the hermetic launcher The previous commit put the hermetic node launcher on the bash launcher's PATH, which made every js_binary depend on hermetic_launcher: the stub was in every target's runfiles so it was always stamped, the launcher template toolchain was mandatory so js_binary stopped building for the platforms hermetic_launcher has no stub for, and the stub landed in js_image_layer output so the listing goldens churned. Generate both launchers, but keep them independent. The bash launcher goes back to its own `node_bin/node` shell wrapper and is otherwise untouched -- its code in js_binary.bzl is unchanged, this commit only adds to that file. The hermetic node launcher is still stamped per target, but it is kept out of runfiles and exposed only through a `hermetic_node_launcher` output group, so the action does not run unless something asks for it. That leaves a path to migrate targets across one at a time without imposing anything on anyone still using the bash launcher. Declaring the two launcher toolchains optional is what makes that free. It also undoes the platform regression: linux ppc64le, windows arm64 and iOS build normally again, so the substitute toolchain that e2e/no_shell_toolchain needed is reverted along with the js_image_layer listing changes. Kept from the previous commit is the runfiles-relative spelling of JS_BINARY__NODE_PATCHES. It is inert while the shell wrapper is what ends up on the PATH, but it is what lets a caller adopt the hermetic launcher without node applying the fs patches twice. --- ### Changes are visible to end-users: yes ### Test plan - Covered by existing test cases - New test cases added - js/private/test/hermetic_launcher runs the launcher binary with no bash launcher involved and asserts the patches were preloaded; the filegroup there doubles as the example of how to request the output group - Verified a clean build stamps no launcher, and that --output_groups=hermetic_node_launcher does Co-Authored-By: Claude Opus 5 (1M context) --- e2e/no_shell_toolchain/BUILD.bazel | 13 ---- e2e/no_shell_toolchain/MODULE.bazel | 2 - js/defs.bzl | 2 +- js/private/BUILD.bazel | 2 + js/private/js_binary.bzl | 73 +++++++++++++++---- js/private/js_binary.sh.tpl | 2 +- js/private/node_bin/node | 5 ++ js/private/node_bin_windows/node.bat | 3 + js/private/test/hermetic_launcher/BUILD.bazel | 42 +++++++++++ .../test/hermetic_launcher/launcher_test.sh | 36 +++++++++ js/private/test/hermetic_launcher/probe.js | 6 ++ js/private/test/image/asserts.bzl | 11 +-- .../test/image/custom_owner_test_app.listing | 4 +- .../test/image/default_test_app.listing | 4 +- .../custom_layer_groups_test_app.listing | 4 +- .../rspack_linux_arm64_test_app.listing | 4 +- .../image/regex_edge_cases_test_app.listing | 4 +- js/private/test/snapshots/launcher.sh | 4 +- 18 files changed, 166 insertions(+), 55 deletions(-) create mode 100755 js/private/node_bin/node create mode 100755 js/private/node_bin_windows/node.bat create mode 100644 js/private/test/hermetic_launcher/BUILD.bazel create mode 100755 js/private/test/hermetic_launcher/launcher_test.sh create mode 100644 js/private/test/hermetic_launcher/probe.js diff --git a/e2e/no_shell_toolchain/BUILD.bazel b/e2e/no_shell_toolchain/BUILD.bazel index 8e93252255..370f034301 100644 --- a/e2e/no_shell_toolchain/BUILD.bazel +++ b/e2e/no_shell_toolchain/BUILD.bazel @@ -46,16 +46,3 @@ toolchain( toolchain = "@nodejs_linux_amd64//:toolchain", toolchain_type = "@rules_nodejs//nodejs:runtime_toolchain_type", ) - -# Likewise for the launcher template that js_binary stamps its node launcher from. -# hermetic_launcher ships no iOS stub; as with the Node.js runtime above, any -# platform's works here since the js_binary is only built, never run. -toolchain( - name = "launcher_template_ios_arm64_toolchain", - target_compatible_with = [ - "@platforms//os:ios", - "@platforms//cpu:arm64", - ], - toolchain = "@hermetic_launcher//launcher/private/prebuilt:template_x86_64_linux_prebuilt", - toolchain_type = "@hermetic_launcher//launcher:template_toolchain_type", -) diff --git a/e2e/no_shell_toolchain/MODULE.bazel b/e2e/no_shell_toolchain/MODULE.bazel index 5da90a88a3..b3e7bf8d53 100644 --- a/e2e/no_shell_toolchain/MODULE.bazel +++ b/e2e/no_shell_toolchain/MODULE.bazel @@ -4,7 +4,6 @@ bazel_dep(name = "aspect_rules_js", version = "0.0.0", dev_dependency = True) bazel_dep(name = "bazel_skylib", version = "1.8.1", dev_dependency = True) bazel_dep(name = "platforms", version = "0.0.11", dev_dependency = True) bazel_dep(name = "rules_nodejs", version = "6.6.0", dev_dependency = True) -bazel_dep(name = "hermetic_launcher", version = "0.0.15", dev_dependency = True) local_path_override( module_name = "aspect_rules_js", @@ -19,7 +18,6 @@ use_repo(node, "nodejs_linux_amd64") register_toolchains( "//:nodejs_ios_arm64_toolchain", - "//:launcher_template_ios_arm64_toolchain", dev_dependency = True, ) diff --git a/js/defs.bzl b/js/defs.bzl index 9a1a4dbe50..6add5fd781 100644 --- a/js/defs.bzl +++ b/js/defs.bzl @@ -76,7 +76,7 @@ def js_binary(**kwargs): * JS_BINARY__NODE_BINARY: the Node.js binary path run by the `js_binary` target * JS_BINARY__NPM_BINARY: the npm binary path; this is available when `include_npm` is `True` on the `js_binary` target - * JS_BINARY__NODE_WRAPPER: the Node.js launcher binary used to run Node.js which is available as `node` on the `PATH` at runtime + * JS_BINARY__NODE_WRAPPER: the Node.js wrapper script used to run Node.js which is available as `node` on the `PATH` at runtime * JS_BINARY__RUNFILES: the absolute path to the Bazel runfiles directory * JS_BINARY__EXECROOT: the absolute path to the root of the execution root for the action; if in the sandbox, this path absolute path to the root of the execution root within the sandbox diff --git a/js/private/BUILD.bazel b/js/private/BUILD.bazel index e4cd4ff68a..ade840e4e2 100644 --- a/js/private/BUILD.bazel +++ b/js/private/BUILD.bazel @@ -6,6 +6,8 @@ package(default_visibility = ["//visibility:public"]) exports_files([ "js_binary.sh.tpl", + "node_bin/node", + "node_bin_windows/node.bat", "npm_bin/npm", "npm_bin_windows/npm.bat", "js_image_layer.mjs", diff --git a/js/private/js_binary.bzl b/js/private/js_binary.bzl index 77527b7008..8b5cce5c47 100644 --- a/js/private/js_binary.bzl +++ b/js/private/js_binary.bzl @@ -250,8 +250,17 @@ _ATTRS = { default = Label("//js/private:js_binary.sh.tpl"), allow_single_file = True, ), - # Windows gets its own separate directory for the npm wrapper. This ensures - # that the bash script does not end up on the PATH when we build for Windows. + # Windows gets its own separate directory for node and npm wrappers. This + # ensures that the bash scripts do not end up on the PATH when we build for + # Windows. + "_node_wrapper_sh": attr.label( + default = Label("//js/private:node_bin/node"), + allow_single_file = True, + ), + "_node_wrapper_bat": attr.label( + default = Label("//js/private:node_bin_windows/node.bat"), + allow_single_file = True, + ), "_npm_wrapper_sh": attr.label( default = Label("//js/private:npm_bin/npm"), allow_single_file = True, @@ -327,12 +336,36 @@ def _rlocation_path(ctx, file): return file.short_path[3:] return "{}/{}".format(ctx.workspace_name, file.short_path) -def _compile_node_launcher(ctx, nodeinfo, output, args): - """Compiles a launcher binary that runs node with `args` prepended to its argv. +def _hermetic_node_launcher(ctx, nodeinfo, is_windows): + """Stamps a launcher binary that runs node with the fs patches applied. + + This is an alternative to the `node_bin/node` shell wrapper, intended to be put on + the PATH as `node` so that child processes get the patched runtime without needing + a shell. It is independent of the bash launcher, which still uses the shell + wrapper, and is exposed only through the `hermetic_node_launcher` output group so + that the stamping action does not run unless something asks for it. - Each entry of `args` is either a File, which is resolved through runfiles when the - launcher runs, or a string, which is embedded verbatim. + Returns None when hermetic_launcher publishes no stub for the target platform + (e.g. linux ppc64le, windows arm64), so that those platforms keep building. """ + if not ctx.toolchains[_TEMPLATE_TOOLCHAIN_TYPE] or not ctx.toolchains[_FINALIZER_TOOLCHAIN_TYPE]: + return None + + # Windows needs the .exe suffix for this to be found by a bare `node` on the PATH. + # It gets a directory to itself because whoever puts it on the PATH will add that + # directory, and nothing else in it may shadow another tool. + # + # NB: a js_binary named `node_bin` cannot work, since the launcher would be the + # file `node_bin_/node_bin` and this the directory `node_bin_/node_bin/`. + output = ctx.actions.declare_file("{}_/node_bin/node{}".format( + ctx.label.name, + ".exe" if is_windows else "", + )) + + # Each entry is either a File, resolved through runfiles when the launcher runs, or + # a string, embedded verbatim. + args = ["--require", ctx.file._node_patches] + if nodeinfo.node: embedded_args, transformed_args = launcher.args_from_entrypoint(nodeinfo.node) elif nodeinfo.node_path.startswith("/"): @@ -360,6 +393,7 @@ def _compile_node_launcher(ctx, nodeinfo, output, args): ) _compile_stub(ctx, embedded_args, transformed_args, output) + return output def _bash_launcher(ctx, nodeinfo, entry_point_path, log_prefix_rule_set, log_prefix_rule, fixed_args, fixed_env, is_windows): # Explicitly disable node fs patches on Windows: @@ -454,14 +488,7 @@ def _bash_launcher(ctx, nodeinfo, entry_point_path, log_prefix_rule_set, log_pre if ctx.attr.expand_args: fixed_args = [expand_variables(ctx, expand_locations(ctx, fixed_arg, ctx.attr.data)) for fixed_arg in fixed_args] - # Windows needs the .exe suffix for this to be found by a bare `node` on the PATH. - # The launcher script puts the parent directory on the PATH, so the launcher gets a - # directory to itself and cannot shadow the npm wrapper sitting next to it. - node_wrapper = ctx.actions.declare_file("{}_/node_bin/node{}".format( - ctx.label.name, - ".exe" if is_windows else "", - )) - _compile_node_launcher(ctx, nodeinfo, node_wrapper, ["--require", ctx.file._node_patches]) + node_wrapper = ctx.file._node_wrapper_bat if is_windows else ctx.file._node_wrapper_sh toolchain_files = [node_wrapper] npm_path = "" @@ -574,6 +601,9 @@ def _create_launcher(ctx, log_prefix_rule_set, log_prefix_rule, fixed_args = [], executable = launcher, runfiles = runfiles, data_runfiles = data_runfiles, + # Deliberately not in runfiles: nothing builds this unless it is requested + # through the output group of the same name. + hermetic_node_launcher = _hermetic_node_launcher(ctx, nodeinfo, is_windows), ) def _js_binary_impl(ctx): @@ -651,6 +681,13 @@ def _js_binary_impl(ctx): # toolchain scaffolding. Consumed by js_run_binary when # use_execroot_entry_point is enabled. execroot_data_files = launcher.data_runfiles.files, + # A `node` that applies the fs patches, as a launcher binary rather than a + # shell script. Not used by the bash launcher and not in runfiles, so it + # only gets stamped when explicitly requested. Empty on platforms + # hermetic_launcher publishes no stub for. + hermetic_node_launcher = depset( + [launcher.hermetic_node_launcher] if launcher.hermetic_node_launcher else [], + ), ), ] @@ -697,8 +734,12 @@ js_binary_lib = struct( # Optional: only referenced on Windows config_common.toolchain_type("@bazel_tools//tools/sh:toolchain_type", mandatory = False), "@rules_nodejs//nodejs:runtime_toolchain_type", - _FINALIZER_TOOLCHAIN_TYPE, - _TEMPLATE_TOOLCHAIN_TYPE, + # Optional: only needed to stamp the hermetic_node_launcher output group, and + # hermetic_launcher publishes no stub for some platforms rules_js supports + # (linux ppc64le, windows arm64). Requiring these would stop js_binary from + # building there at all. + config_common.toolchain_type(_FINALIZER_TOOLCHAIN_TYPE, mandatory = False), + config_common.toolchain_type(_TEMPLATE_TOOLCHAIN_TYPE, mandatory = False), ] + COPY_FILE_TO_BIN_TOOLCHAINS, ) diff --git a/js/private/js_binary.sh.tpl b/js/private/js_binary.sh.tpl index 5fe8518766..6b2a82e301 100644 --- a/js/private/js_binary.sh.tpl +++ b/js/private/js_binary.sh.tpl @@ -336,7 +336,7 @@ fi if [ "${JS_BINARY__NO_RUNFILES:-}" ]; then export JS_BINARY__NODE_WRAPPER - JS_BINARY__NODE_WRAPPER=$(resolve_execroot_bin_path "{{node_wrapper}}") + JS_BINARY__NODE_WRAPPER=$(resolve_execroot_src_path "{{node_wrapper}}") else export JS_BINARY__NODE_WRAPPER="$JS_BINARY__RUNFILES/{{workspace_name}}/{{node_wrapper}}" fi diff --git a/js/private/node_bin/node b/js/private/node_bin/node new file mode 100755 index 0000000000..fae2c2a56f --- /dev/null +++ b/js/private/node_bin/node @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -o pipefail -o errexit -o nounset + +exec "$JS_BINARY__NODE_BINARY" --require "$JS_BINARY__NODE_PATCHES" "$@" diff --git a/js/private/node_bin_windows/node.bat b/js/private/node_bin_windows/node.bat new file mode 100755 index 0000000000..85ad5d2bf7 --- /dev/null +++ b/js/private/node_bin_windows/node.bat @@ -0,0 +1,3 @@ +@if not defined DEBUG_HELPER @ECHO OFF + +%JS_BINARY__NODE_BINARY% --require %JS_BINARY__NODE_PATCHES% %* diff --git a/js/private/test/hermetic_launcher/BUILD.bazel b/js/private/test/hermetic_launcher/BUILD.bazel new file mode 100644 index 0000000000..e10151c3fb --- /dev/null +++ b/js/private/test/hermetic_launcher/BUILD.bazel @@ -0,0 +1,42 @@ +load("@rules_shell//shell:sh_test.bzl", "sh_test") +load("//js:defs.bzl", "js_binary") + +# The hermetic node launcher is not part of any js_binary's runfiles and nothing +# builds it by default, so it needs a target of its own to be covered at all. + +package(default_testonly = True) + +js_binary( + name = "bin", + entry_point = "probe.js", +) + +# Pulls the launcher binary out of the output group. This is also the worked +# example of how a caller opts into it. +filegroup( + name = "node_launcher", + srcs = [":bin"], + output_group = "hermetic_node_launcher", +) + +# `bin` is in data so that node, bootstrap.cjs and fs.cjs land in this test's +# runfiles at the rlocation paths the launcher has baked in. +sh_test( + name = "node_launcher_test", + srcs = ["launcher_test.sh"], + args = [ + "$(rootpath :node_launcher)", + "$(rootpath probe.js)", + ], + data = [ + "probe.js", + ":bin", + ":node_launcher", + ], + # The launcher is untested on Windows, where it must resolve runfiles through + # RUNFILES_MANIFEST_FILE rather than a materialized tree. + target_compatible_with = select({ + "@platforms//os:windows": ["@platforms//:incompatible"], + "//conditions:default": [], + }), +) diff --git a/js/private/test/hermetic_launcher/launcher_test.sh b/js/private/test/hermetic_launcher/launcher_test.sh new file mode 100755 index 0000000000..a4bd0c6afe --- /dev/null +++ b/js/private/test/hermetic_launcher/launcher_test.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Runs the hermetic node launcher directly, with no bash launcher involved, and +# checks that it exec'd node with the fs patches preloaded. + +set -o errexit -o nounset -o pipefail + +launcher="$1" +probe="$2" + +if [ ! -x "$launcher" ]; then + echo "FAIL: '$launcher' is not executable" >&2 + exit 1 +fi + +# The launcher resolves its embedded rlocation paths out of the runfiles tree; it +# reads these two only to decide whether to apply the fs patches. +export JS_BINARY__PATCH_NODE_FS=1 +export JS_BINARY__FS_PATCH_ROOTS="$PWD" + +output="$("$launcher" "$probe")" + +# bootstrap.cjs sets the depth marker, so a value here means it was required. +if [[ "$output" != *"depth=."* ]]; then + echo "FAIL: expected the node patches to be preloaded, got:" >&2 + echo "$output" >&2 + exit 1 +fi + +if [[ "$output" != *"fs_patched=yes"* ]]; then + echo "FAIL: expected the fs patches to be applied, got:" >&2 + echo "$output" >&2 + exit 1 +fi + +echo "PASS" diff --git a/js/private/test/hermetic_launcher/probe.js b/js/private/test/hermetic_launcher/probe.js new file mode 100644 index 0000000000..9af73f3b09 --- /dev/null +++ b/js/private/test/hermetic_launcher/probe.js @@ -0,0 +1,6 @@ +// Reports whether the node patches were preloaded, so the test can tell that the +// hermetic node launcher really did exec node with `--require `. +const fs = require('fs') + +console.log('depth=' + process.env.JS_BINARY__NODE_PATCHES_DEPTH) +console.log('fs_patched=' + (fs._unpatched ? 'yes' : 'no')) diff --git a/js/private/test/image/asserts.bzl b/js/private/test/image/asserts.bzl index 4b8d222f2d..de5fe49c1e 100644 --- a/js/private/test/image/asserts.bzl +++ b/js/private/test/image/asserts.bzl @@ -13,21 +13,12 @@ SKIP_ON_WINDOWS = select({ # js_binary launcher scripts have unstable sizes across Bazel versions. _UNSTABLE_SIZE_BASENAMES = ["bin", "bin2"] -# The node launcher binary is stamped from a prebuilt hermetic_launcher stub, so its -# size moves with the stub's platform and version. Matched by path rather than -# basename so the real node binary's size stays pinned. -_UNSTABLE_SIZE_PATHS = ["node_bin/node"] - # buildifier: disable=function-docstring def assert_tar_listing(name, actual, expected): launcher_alt = "|".join(_UNSTABLE_SIZE_BASENAMES) - stub_alt = "|".join([p.replace("/", "\\/") for p in _UNSTABLE_SIZE_PATHS]) # `$$` escapes `$` for Bazel genrule cmd Make-variable expansion. - size_sanitize = ( - "sed -E -e '/\\/({})$$/ s/[0-9]+ Jan/xxxxx Jan/'".format(launcher_alt) + - " -e '/_\\/({})$$/ s/[0-9]+ Jan/xxxxx Jan/'".format(stub_alt) - ) + size_sanitize = "sed -E '/\\/({})$$/ s/[0-9]+ Jan/xxxxx Jan/'".format(launcher_alt) # Normalize bzlmod canonical repo separators: ~~/~ (Bazel 7) -> ++/+ (Bazel 8+). repo_sep_normalize = ( diff --git a/js/private/test/image/custom_owner_test_app.listing b/js/private/test/image/custom_owner_test_app.listing index ff8cf3a3d3..586923d893 100644 --- a/js/private/test/image/custom_owner_test_app.listing +++ b/js/private/test/image/custom_owner_test_app.listing @@ -7,6 +7,8 @@ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runf drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/ +drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/node_bin/ +-r-xr-xr-x 0 100 0 133 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/node_bin/node drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image-fixture-a/ @@ -15,6 +17,4 @@ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runf -r-xr-xr-x 0 100 0 168 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image-fixture-d/package.json drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/ -r-xr-xr-x 0 100 0 xxxxx Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/bin -drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/node_bin/ --r-xr-xr-x 0 100 0 xxxxx Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/node_bin/node -r-xr-xr-x 0 100 0 20 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/main.js diff --git a/js/private/test/image/default_test_app.listing b/js/private/test/image/default_test_app.listing index 59e5cfe748..917b36bb25 100644 --- a/js/private/test/image/default_test_app.listing +++ b/js/private/test/image/default_test_app.listing @@ -7,6 +7,8 @@ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runf drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/ +drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/node_bin/ +-r-xr-xr-x 0 0 0 133 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/node_bin/node drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image-fixture-a/ @@ -15,6 +17,4 @@ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runf -r-xr-xr-x 0 0 0 168 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image-fixture-d/package.json drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/ -r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/bin -drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/node_bin/ --r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/node_bin/node -r-xr-xr-x 0 0 0 20 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/main.js diff --git a/js/private/test/image/non_ascii/custom_layer_groups_test_app.listing b/js/private/test/image/non_ascii/custom_layer_groups_test_app.listing index c9a459d00e..ec53185d64 100644 --- a/js/private/test/image/non_ascii/custom_layer_groups_test_app.listing +++ b/js/private/test/image/non_ascii/custom_layer_groups_test_app.listing @@ -9,13 +9,13 @@ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/ +drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/node_bin/ +-r-xr-xr-x 0 0 0 133 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/node_bin/node drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/ -r-xr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/ㅑㅕㅣㅇ.ㄴㅅ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/bin2_/ -r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/bin2_/bin2 -drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/bin2_/node_bin/ --r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/bin2_/node_bin/node -r-xr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/empty empty.ㄴㅅ -r-xr-xr-x 0 0 0 20 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/main.js diff --git a/js/private/test/image/platform_deps/rspack_linux_arm64_test_app.listing b/js/private/test/image/platform_deps/rspack_linux_arm64_test_app.listing index 6cd9488b92..dbbada3c00 100644 --- a/js/private/test/image/platform_deps/rspack_linux_arm64_test_app.listing +++ b/js/private/test/image/platform_deps/rspack_linux_arm64_test_app.listing @@ -9,11 +9,11 @@ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/plat drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/ +drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/node_bin/ +-r-xr-xr-x 0 0 0 133 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/node_bin/node drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/test/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/test/image/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/test/image/platform_deps/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/test/image/platform_deps/bin_/ -r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/test/image/platform_deps/bin_/bin -drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/test/image/platform_deps/bin_/node_bin/ --r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/test/image/platform_deps/bin_/node_bin/node -r-xr-xr-x 0 0 0 20 Jan 1 1970 ./app/js/private/test/image/platform_deps/bin.runfiles/_main/js/private/test/image/platform_deps/main.js diff --git a/js/private/test/image/regex_edge_cases_test_app.listing b/js/private/test/image/regex_edge_cases_test_app.listing index 52196f5638..e3d8f194fe 100644 --- a/js/private/test/image/regex_edge_cases_test_app.listing +++ b/js/private/test/image/regex_edge_cases_test_app.listing @@ -8,6 +8,8 @@ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin. drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/ +drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/node_bin/ +-r-xr-xr-x 0 0 0 133 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/node_bin/node drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image-fixture-a/ @@ -16,6 +18,4 @@ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin. -r-xr-xr-x 0 0 0 168 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image-fixture-d/package.json drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/ -r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/bin -drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/node_bin/ --r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/node_bin/node -r-xr-xr-x 0 0 0 20 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/main.js diff --git a/js/private/test/snapshots/launcher.sh b/js/private/test/snapshots/launcher.sh index a4d1687b77..ddd9a2358e 100644 --- a/js/private/test/snapshots/launcher.sh +++ b/js/private/test/snapshots/launcher.sh @@ -456,9 +456,9 @@ fi if [ "${JS_BINARY__NO_RUNFILES:-}" ]; then export JS_BINARY__NODE_WRAPPER - JS_BINARY__NODE_WRAPPER=$(resolve_execroot_bin_path "js/private/test/shellcheck_launcher_/node_bin/node") + JS_BINARY__NODE_WRAPPER=$(resolve_execroot_src_path "js/private/node_bin/node") else - export JS_BINARY__NODE_WRAPPER="$JS_BINARY__RUNFILES/_main/js/private/test/shellcheck_launcher_/node_bin/node" + export JS_BINARY__NODE_WRAPPER="$JS_BINARY__RUNFILES/_main/js/private/node_bin/node" fi if [ ! -f "$JS_BINARY__NODE_WRAPPER" ]; then logf_fatal "node wrapper '%s' not found" "$JS_BINARY__NODE_WRAPPER"