blob: 964f379ee601e7d0604726950753ae766e6732c0 [file]
// This JavaScript file is the launcher for the NodeJS JavaScript file
// entry point with the following bazel label:
// {{entry_point_label}}
//
// The launcher was generated to execute the js_binary target
// {{target_label}}
//
// The template used to generate this launcher is
// {{template_label}}
//
// It is the hermetic launcher's counterpart to js_binary.sh.tpl, and does the same job: work
// out where node, the entry point and the per-launch preload are, set the environment the
// target asked for, and start node on them. Everything that can wait until node is up lives in
// js/private/node-bootstrap/launcher.cjs, which both launchers load as node's first --require.
'use strict'
// Replaced by js_image_layer to make the launcher hermetic; the env block below reads it.
const JS_IMAGE_LAYER = false
const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
// ==============================================================================
// Values baked in at analysis time
// ==============================================================================
const WORKSPACE_NAME = {{workspace_name}}
const ENTRY_POINT_PATH = {{entry_point_path}}
const NODE_PATH = {{node}}
const NPM_PATH = {{npm}}
const LOG_PREFIX_RULE_SET = {{log_prefix_rule_set}}
const LOG_PREFIX_RULE = {{log_prefix_rule}}
// The node options the launcher's own process was already started with, by the native stub.
const STUB_NODE_OPTIONS = {{stub_node_options}}
// Worked out at analysis time; see _env_configures_node_startup in js/private/js_binary.bzl.
const ENV_CONFIGURES_NODE_STARTUP = {{env_configures_node_startup}}
// ==============================================================================
// Shared helpers
// ==============================================================================
// The per-launch preload, resolved out of the runfiles by the stub and handed to this
// launcher as its first argument. The helpers both launchers share sit beside it.
//
// Made absolute before the chdir below moves it: the stub resolves an rlocation to a relative
// path when it was given a relative runfiles directory, and a relative specifier with no
// leading "./" is a package name to require().
const launcher = path.resolve(process.argv[2])
const {
checkExecutableFile,
exitWith,
expandEnvRefs,
forwardSignals,
isDirectory,
logDebug,
logError,
logFatal,
logInfo,
resolveExecrootBinPath,
resolveToolchainPath,
setEnv,
setEnvIfUnset,
withSlashes,
} = require(path.join(path.dirname(launcher), 'util.cjs'))
// ==============================================================================
// Environment
// ==============================================================================
{{envs}}
// ==============================================================================
// Handle --bazel-bindir flag
// ==============================================================================
// If a --bazel-bindir <path> flag is passed it must be the first two
// arguments. It is consumed by this launcher and used to set BAZEL_BINDIR,
// overriding any value already set in the environment.
//
// The caller's arguments start at index 3: node's own argv[0] and argv[1], then the preload
// the stub resolved above.
const argv = process.argv.slice(3)
if (argv.length > 0 && argv[0] === '--bazel-bindir') {
if (argv.length < 2) {
fs.writeSync(2, 'ERROR: --bazel-bindir flag requires a value\n')
process.exit(1)
}
process.env.BAZEL_BINDIR = argv[1]
argv.splice(0, 2)
}
// ==============================================================================
// Prepare logging
// ==============================================================================
// Read by the shared log functions on every call, so it has to be set before the first of
// them runs.
process.env.JS_BINARY__LOG_PREFIX = `${LOG_PREFIX_RULE_SET}[${LOG_PREFIX_RULE}]`
// ==============================================================================
// Runfiles initialization
// ==============================================================================
// In manifest-only mode the stub resolves its inputs without setting RUNFILES_DIR.
// Match the Bash launcher's nominal runfiles directory for the program's environment.
const manifest = process.env.RUNFILES_MANIFEST_FILE
const runfiles =
process.env.RUNFILES_DIR ||
(manifest && manifest.replace(/(?:_manifest|[/\\]MANIFEST)$/, ''))
if (!runfiles) {
logFatal('RUNFILES_DIR environment variable is not set')
exitWith(1)
}
// JS_BINARY__RUNFILES is documented to be an absolute path to the runfiles
// directory, so we need to uphold that guarantee.
process.env.JS_BINARY__RUNFILES = withSlashes(path.resolve(runfiles))
// ==============================================================================
// Prepare to run main program
// ==============================================================================
// The execroot the entry point is resolved against below. A parent js_binary process hands its
// own down when it asks for an execroot entry point, because the entry point it resolved is in
// that tree; otherwise this launcher was started in it. Nothing else here needs an execroot:
// the preload works out JS_BINARY__EXECROOT for the program and everything it spawns.
const execroot =
process.env.JS_BINARY__USE_EXECROOT_ENTRY_POINT &&
process.env.JS_BINARY__EXECROOT
? withSlashes(process.env.JS_BINARY__EXECROOT)
: withSlashes(process.cwd())
// Build actions are started in the execroot, so change into the root of the Bazel output tree,
// which is where js_binary programs run. See
// https://github.com/aspect-build/rules_js/tree/dbb5af0d2a9a2bb50e4cf4a96dbc582b27567155#running-nodejs-programs
// for more context on why we do this. It cannot wait for the preload: node resolves the bare
// specifier of a --require in node_options against the directory it was started in.
//
// The bindir is only there to change into when this really is an execroot; in a runfiles tree,
// or in a nested js_binary already running in the bindir, there is nothing to do. The preload
// tells those apart from the broken case by JS_BINARY__CHANGED_TO_BINDIR.
if (
!process.env.JS_BINARY__NO_CD_BINDIR &&
process.env.BAZEL_BINDIR &&
isDirectory(path.join(process.cwd(), process.env.BAZEL_BINDIR))
) {
logDebug(
`changing directory to BAZEL_BINDIR (root of Bazel output tree) ${process.env.BAZEL_BINDIR}`
)
process.env.JS_BINARY__CHANGED_TO_BINDIR = '1'
process.chdir(process.env.BAZEL_BINDIR)
process.env.PWD = process.cwd()
}
if (
process.env.JS_BINARY__USE_EXECROOT_ENTRY_POINT &&
!process.env.BAZEL_BINDIR
) {
logFatal(
'Expected BAZEL_BINDIR to be set when JS_BINARY__USE_EXECROOT_ENTRY_POINT is set'
)
exitWith(1)
}
// Everything below is resolved here only because node needs it on its command line
// or this launcher needs it to start node at all. The checks that can wait, and the rest of
// the environment the program sees, are done by the preload once node is up.
let entryPoint
if (process.env.JS_BINARY__USE_EXECROOT_ENTRY_POINT) {
entryPoint = resolveExecrootBinPath(execroot, ENTRY_POINT_PATH)
} else if (process.env.JS_BINARY__NO_RUNFILES) {
// Resolve the tool in its own configuration without changing the action's cwd.
entryPoint = resolveExecrootBinPath(
execroot,
ENTRY_POINT_PATH,
process.env.JS_BINARY__BINDIR
)
} else {
entryPoint = `${process.env.JS_BINARY__RUNFILES}/${WORKSPACE_NAME}/${ENTRY_POINT_PATH}`
}
process.env.JS_BINARY__NODE_BINARY = resolveToolchainPath(
execroot,
WORKSPACE_NAME,
NODE_PATH
)
checkExecutableFile('node binary', process.env.JS_BINARY__NODE_BINARY)
if (NPM_PATH) {
process.env.JS_BINARY__NPM_BINARY = resolveToolchainPath(
execroot,
WORKSPACE_NAME,
NPM_PATH
)
}
// Gather node options
const nodeOptions = []
function addNodeOption(value) {
nodeOptions.push(expandEnvRefs(value))
}
{{node_options}}
// fixed_args were tokenized at analysis time, each token as a list of [text, expand]
// segments: the quotes are gone but this launcher still has to know which were single
// quotes, since bash would not have expanded $VAR inside those. Expansion happens now.
const FIXED_ARGS = {{fixed_args}}.map((segments) =>
segments.map(([text, expand]) => (expand ? expandEnvRefs(text) : text)).join('')
)
const args = []
for (const arg of [...FIXED_ARGS, ...argv]) {
if (arg.startsWith('--node_options=')) {
// Let users pass through arguments to node itself
nodeOptions.push(arg.slice('--node_options='.length))
} else {
// Remaining argv is collected to pass to the program
args.push(arg)
}
}
// ==============================================================================
// Run the main program
// ==============================================================================
const expectedExitCode = process.env.JS_BINARY__EXPECTED_EXIT_CODE
// If possible, we go straight to the entry point without exec'ing node again. However, we
// need to re-launch node if expected_exit_code is used, if we set any environment variables
// that affect node at startup, or if we are using any node options that were not already set
// on the current process.
const runInProcess =
!ENV_CONFIGURES_NODE_STARTUP &&
!expectedExitCode &&
nodeOptions.length === STUB_NODE_OPTIONS.length &&
nodeOptions.every((option, i) => option === STUB_NODE_OPTIONS[i])
if (runInProcess) {
if (process.env.JS_BINARY__LOG_INFO) {
logInfo(['running in this process', entryPoint, ...args].join(' '))
}
process.argv = [process.argv[0], entryPoint, ...args]
// node itself was only given STUB_NODE_OPTIONS, because the preload is required below
// rather than passed on a command line. Report the arguments the exec path would have
// used: the preload reads its own location out of execArgv[1], and a tool forwarding
// execArgv to a child still reproduces the patched runtime.
process.execArgv = ['--require', launcher, ...nodeOptions]
// The per-launch setup node would have run as its first --require on the exec path. It
// is required here rather than passed to the stub because it reads JS_BINARY__ variables
// that only exist once the launcher above has run.
require(launcher)
// A worker thread is started with the exec arguments node itself was given, which on this
// path carry no --require at all -- the preload above was reached by require(), not by a
// command line, so a worker would otherwise run with unpatched fs. A program that asks
// for its own exec arguments is left alone.
const workerThreads = require('node:worker_threads')
const RealWorker = workerThreads.Worker
// The node options too, not just the bootstrap: on the exec path a worker inherits node's
// whole command line, so dropping them here would lose --preserve-symlinks-main and let a
// worker's entry point resolve out of the runfiles tree (aspect-build/rules_js#362).
const workerExecArgv = [
'--require',
process.env.JS_BINARY__NODE_PATCHES,
...nodeOptions,
]
workerThreads.Worker = class Worker extends RealWorker {
constructor(filename, options) {
super(
filename,
options && options.execArgv
? options
: { ...options, execArgv: workerExecArgv }
)
}
}
// Runs the entry point as the main module, so that `require.main === module` holds for
// it. This returns as soon as the entry point's top level does, and node then exits on
// its own once the event loop drains. Nothing may follow it here -- an exit would
// truncate every asynchronous program.
require('node:module').runMain()
} else {
// We invoke node directly rather than through JS_BINARY__NODE_WRAPPER. This
// way we avoid spawning an extra bash process on every launch. The wrapper is
// still put on the PATH as `node` so that child processes get the patched
// runtime.
const nodeArgs = [
'--require',
launcher,
...nodeOptions,
'--',
entryPoint,
...args,
]
if (process.env.JS_BINARY__LOG_INFO) {
logInfo(['running', process.env.JS_BINARY__NODE_BINARY, ...nodeArgs].join(' '))
}
if (!expectedExitCode) {
// Nothing must run after node exits, so replace this process with node.
// Signals and terminal control are then delivered directly to node instead
// of being proxied through a child process, and no launcher process is left
// behind.
//
// process.execve is POSIX-only and was added in Node 22.15; when it is
// unavailable we fall through to spawning node below.
if (typeof process.execve === 'function') {
try {
process.execve(
process.env.JS_BINARY__NODE_BINARY,
[process.env.JS_BINARY__NODE_BINARY, ...nodeArgs],
{ ...process.env }
)
} catch (e) {
logDebug(`process.execve failed (${e.message}); falling back to spawn`)
}
}
}
// Reached when this launcher has to outlive the program: an expected exit code has to be
// compared against once the program is done, and a Node before 22.15, or any Node on
// Windows, has no process.execve to replace this process with.
const { spawn } = require('node:child_process')
const child = spawn(process.env.JS_BINARY__NODE_BINARY, nodeArgs, {
stdio: 'inherit',
})
// ==============================================================================
// Wait for program to finish
// ==============================================================================
const signals = forwardSignals(child)
child.on('error', (err) => {
logFatal(
`failed to spawn node binary '${process.env.JS_BINARY__NODE_BINARY}': ${err.message}`
)
exitWith(127)
})
child.on('exit', (code, signal) => {
const result =
signal !== null && signal !== undefined
? 128 + (os.constants.signals[signal] || 0)
: code
// ==============================================================================
// Mop up after main program
// ==============================================================================
if (expectedExitCode) {
if (String(result) !== String(expectedExitCode)) {
logError(
`expected exit code to be '${expectedExitCode}', but got '${result}'`
)
if (result === 0) {
// This exit code is handled specially by Bazel:
// https://github.com/bazelbuild/bazel/blob/486206012a664ecb20bdb196a681efc9a9825049/src/main/java/com/google/devtools/build/lib/util/ExitCode.java#L44
const BAZEL_EXIT_TESTS_FAILED = 3
exitWith(BAZEL_EXIT_TESTS_FAILED)
}
exitWith(result)
} else {
exitWith(0)
}
}
if (signal) {
signals.reraise(signal, result)
} else {
exitWith(result)
}
})
}