Skip to content

Commit 38d0b3d

Browse files
gregmagolanalexeagle
authored andcommitted
fix: fix nodejs_binary cross-platform RBE issue #1305
* node_launcher.sh now picks the node binary at runtime instead of it being a templated arg evaluated at build time * node_binary now adds the correct node tool_files to the nodejs_binary runfiles Fixes #1305 fix: node
1 parent 220fa51 commit 38d0b3d

5 files changed

Lines changed: 136 additions & 37 deletions

File tree

internal/common/path_utils.bzl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright 2017 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Path utils
16+
17+
Helper functions for path manipulations.
18+
"""
19+
20+
def strip_external(path):
21+
return path[len("external/"):] if path.startswith("external/") else path

internal/node/node.bzl

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ a `module_name` attribute can be `require`d by that name.
2323
load("@build_bazel_rules_nodejs//:providers.bzl", "JSNamedModuleInfo", "NodeRuntimeDepsInfo", "NpmPackageInfo", "node_modules_aspect")
2424
load("//internal/common:expand_into_runfiles.bzl", "expand_location_into_runfiles")
2525
load("//internal/common:module_mappings.bzl", "module_mappings_runtime_aspect")
26+
load("//internal/common:path_utils.bzl", "strip_external")
2627
load("//internal/common:windows_utils.bzl", "create_windows_native_launcher_script", "is_windows")
28+
load("//internal/node:node_repositories.bzl", "BUILT_IN_NODE_PLATFORMS")
2729

2830
def _trim_package_node_modules(package_name):
2931
# trim a package name down to its path prior to a node_modules
@@ -173,15 +175,17 @@ def _nodejs_binary_impl(ctx):
173175
if hasattr(ctx.attr, "expected_exit_code"):
174176
expected_exit_code = ctx.attr.expected_exit_code
175177

176-
node_tool_info = ctx.toolchains["@build_bazel_rules_nodejs//toolchains/node:toolchain_type"].nodeinfo
177-
178-
# Make a copy so we don't try to mutate a frozen object
179-
node_tool_files = node_tool_info.tool_files[:]
180-
if node_tool_info.target_tool_path == "":
181-
# If tool_path is empty and tool_target is None then there is no local
182-
# node tool, we will just print a nice error message if the user
183-
# attempts to do bazel run
184-
fail("The node toolchain was not properly configured so %s cannot be executed. Make sure that target_tool_path or target_tool is set." % ctx.attr.name)
178+
# Add both the node executable for the user's local machine which is in ctx.files._node and comes
179+
# from @nodejs//:node_bin and the node executable from the selected node --platform which comes from
180+
# ctx.toolchains["@build_bazel_rules_nodejs//toolchains/node:toolchain_type"].nodeinfo.
181+
# In most cases these are the same files but for RBE and when explitely setting --platform for cross-compilation
182+
# any given nodejs_binary should be able to run on both the user's local machine and on the RBE or selected
183+
# platform.
184+
#
185+
# Rules such as nodejs_image should use only ctx.toolchains["@build_bazel_rules_nodejs//toolchains/node:toolchain_type"].nodeinfo
186+
# when building the image as that will reflect the selected --platform.
187+
node_tool_files = ctx.files._node[:]
188+
node_tool_files.extend(ctx.toolchains["@build_bazel_rules_nodejs//toolchains/node:toolchain_type"].nodeinfo.tool_files)
185189

186190
node_tool_files.append(ctx.file._link_modules_script)
187191
node_tool_files.append(ctx.file._bazel_require_script)
@@ -211,6 +215,8 @@ def _nodejs_binary_impl(ctx):
211215
# also be sure to include the params file in the program inputs
212216
node_tool_files.append(ctx.outputs.templated_args_file)
213217

218+
is_builtin = ctx.attr._node.label.workspace_name in ["nodejs_%s" % p for p in BUILT_IN_NODE_PLATFORMS]
219+
214220
substitutions = {
215221
"TEMPLATED_args": " ".join([
216222
expand_location_into_runfiles(ctx, a)
@@ -221,9 +227,9 @@ def _nodejs_binary_impl(ctx):
221227
"TEMPLATED_expected_exit_code": str(expected_exit_code),
222228
"TEMPLATED_link_modules_script": _to_manifest_path(ctx, ctx.file._link_modules_script),
223229
"TEMPLATED_loader_path": script_path,
224-
"TEMPLATED_node": node_tool_info.target_tool_path,
225230
"TEMPLATED_repository_args": _to_manifest_path(ctx, ctx.file._repository_args),
226231
"TEMPLATED_script_path": _to_execroot_path(ctx, ctx.file.entry_point),
232+
"TEMPLATED_vendored_node": "" if is_builtin else strip_external(ctx.file._node.path),
227233
}
228234
ctx.actions.expand_template(
229235
template = ctx.file._launcher_template,
@@ -466,6 +472,10 @@ jasmine_node_test(
466472
default = Label("//internal/node:node_loader.js"),
467473
allow_single_file = True,
468474
),
475+
"_node": attr.label(
476+
default = Label("@nodejs//:node_bin"),
477+
allow_single_file = True,
478+
),
469479
"_repository_args": attr.label(
470480
default = Label("@nodejs//:bin/node_repo_args.sh"),
471481
allow_single_file = True,

internal/node/node_launcher.sh

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,50 @@ TEMPLATED_env_vars
114114
# This redirects to stderr so it doesn't interfere with Bazel's worker protocol
115115
# find . -name thingImLookingFor 1>&2
116116

117-
readonly node=$(rlocation "TEMPLATED_node")
117+
readonly vendored_node="TEMPLATED_vendored_node"
118+
119+
if [ -n "${vendored_node}" ]; then
120+
# Use the vendored node path
121+
readonly node=$(rlocation "${vendored_node}")
122+
123+
if [ ! -f "${node}" ]; then
124+
printf "\n>>>> FAIL: The vendored node binary '${vendored_node}' not found in runfiles. <<<<\n\n" >&2
125+
exit 1
126+
fi
127+
else
128+
# Check environment for which node path to use
129+
unameOut="$(uname -s)"
130+
case "${unameOut}" in
131+
Linux*) machine=linux ;;
132+
Darwin*) machine=darwin ;;
133+
CYGWIN*) machine=windows ;;
134+
MINGW*) machine=windows ;;
135+
MSYS_NT*) machine=windows ;;
136+
*) machine=linux
137+
printf "\nUnrecongized uname '${unameOut}'; defaulting to use node for linux.\n" >&2
138+
printf "Please file an issue to https://github.com/bazelbuild/rules_nodejs/issues if \n" >&2
139+
printf "you would like to add your platform to the supported rules_nodejs node platforms.\n\n" >&2
140+
;;
141+
esac
142+
143+
case "${machine}" in
144+
# The following paths must match up with _download_node in node_repositories
145+
darwin) readonly node_toolchain="nodejs_darwin_amd64/bin/nodejs/bin/node" ;;
146+
windows) readonly node_toolchain="nodejs_windows_amd64/bin/nodejs/node.exe" ;;
147+
*) readonly node_toolchain="nodejs_linux_amd64/bin/nodejs/bin/node" ;;
148+
esac
149+
150+
readonly node=$(rlocation "${node_toolchain}")
151+
152+
if [ ! -f "${node}" ]; then
153+
printf "\n>>>> FAIL: The node binary '${node_toolchain}' not found in runfiles.\n" >&2
154+
printf "This node toolchain was chosen based on your uname '${unameOut}'.\n" >&2
155+
printf "Please file an issue to https://github.com/bazelbuild/rules_nodejs/issues if \n" >&2
156+
printf "you would like to add your platform to the supported rules_nodejs node platforms. <<<<\n\n" >&2
157+
exit 1
158+
fi
159+
fi
160+
118161
readonly repository_args=$(rlocation "TEMPLATED_repository_args")
119162
MAIN=$(rlocation "TEMPLATED_loader_path")
120163
readonly link_modules_script=$(rlocation "TEMPLATED_link_modules_script")

internal/node/node_repositories.bzl

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,14 @@ and expect the file to have sha256sum `09bea8f4ec41e9079fa03093d3b2db7ac5c533185
243243
),
244244
}
245245

246-
NODE_DIR = "bin/nodejs"
247-
YARN_DIR = "bin/yarnpkg"
246+
BUILT_IN_NODE_PLATFORMS = [
247+
"darwin_amd64",
248+
"linux_amd64",
249+
"windows_amd64",
250+
]
251+
252+
NODE_EXTRACT_DIR = "bin/nodejs"
253+
YARN_EXTRACT_DIR = "bin/yarnpkg"
248254

249255
GET_SCRIPT_DIR = """
250256
# From stackoverflow.com
@@ -268,24 +274,25 @@ def _download_node(repository_ctx):
268274
"""
269275
if repository_ctx.attr.vendored_node:
270276
return
271-
if repository_ctx.name == "nodejs":
272-
host = os_name(repository_ctx)
273-
else:
274-
host = repository_ctx.name.split("nodejs_", 1)[1]
277+
278+
# The host is baked into the repository name by design.
279+
# Current these workspaces are:
280+
# @nodejs_PLATFORM where PLATFORM is one of BUILT_IN_NODE_PLATFORMS
281+
host_os = repository_ctx.name.split("nodejs_", 1)[1]
282+
275283
node_version = repository_ctx.attr.node_version
276284
node_repositories = repository_ctx.attr.node_repositories
277285
node_urls = repository_ctx.attr.node_urls
278286

279287
# Download node & npm
280-
node_host_version = "{}-{}".format(node_version, host)
281-
if node_host_version in node_repositories:
282-
filename, strip_prefix, sha256 = node_repositories[node_host_version]
283-
else:
284-
fail("Unknown NodeJS host/version {}".format(node_host_version))
288+
version_host_os = "%s-%s" % (node_version, host_os)
289+
if not version_host_os in node_repositories:
290+
fail("Unknown NodeJS version-host %s" % version_host_os)
291+
filename, strip_prefix, sha256 = node_repositories[version_host_os]
285292

286293
repository_ctx.download_and_extract(
287294
url = [url.format(version = node_version, filename = filename) for url in node_urls],
288-
output = NODE_DIR,
295+
output = NODE_EXTRACT_DIR,
289296
stripPrefix = strip_prefix,
290297
sha256 = sha256,
291298
)
@@ -306,11 +313,11 @@ def _download_yarn(repository_ctx):
306313
if yarn_version in yarn_repositories:
307314
filename, strip_prefix, sha256 = yarn_repositories[yarn_version]
308315
else:
309-
fail("Unknown Yarn version {}".format(yarn_version))
316+
fail("Unknown Yarn version %s" % yarn_version)
310317

311318
repository_ctx.download_and_extract(
312319
url = [url.format(version = yarn_version, filename = filename) for url in yarn_urls],
313-
output = YARN_DIR,
320+
output = YARN_EXTRACT_DIR,
314321
stripPrefix = strip_prefix,
315322
sha256 = sha256,
316323
)
@@ -353,8 +360,8 @@ def _prepare_node(repository_ctx):
353360
"lib/node_modules/npm/bin/npm-cli.js" if not is_windows else "node_modules/npm/bin/npm-cli.js",
354361
] if f])
355362
else:
356-
node_exec = "{}/bin/node".format(NODE_DIR) if not is_windows else "{}/node.exe".format(NODE_DIR)
357-
npm_script = "{}/lib/node_modules/npm/bin/npm-cli.js".format(NODE_DIR) if not is_windows else "{}/node_modules/npm/bin/npm-cli.js".format(NODE_DIR)
363+
node_exec = ("%s/bin/node" % NODE_EXTRACT_DIR) if not is_windows else ("%s/node.exe" % NODE_EXTRACT_DIR)
364+
npm_script = ("%s/lib/node_modules/npm/bin/npm-cli.js" % NODE_EXTRACT_DIR) if not is_windows else ("%s/node_modules/npm/bin/npm-cli.js" % NODE_EXTRACT_DIR)
358365
node_exec_label = node_exec
359366
if repository_ctx.attr.vendored_yarn:
360367
yarn_script = "/".join([f for f in [
@@ -365,7 +372,7 @@ def _prepare_node(repository_ctx):
365372
"bin/yarn.js",
366373
] if f])
367374
else:
368-
yarn_script = "{}/bin/yarn.js".format(YARN_DIR)
375+
yarn_script = "%s/bin/yarn.js" % YARN_EXTRACT_DIR
369376
node_entry = "bin/node" if not is_windows else "bin/node.cmd"
370377
npm_node_repositories_entry = "bin/npm_node_repositories" if not is_windows else "bin/npm_node_repositories.cmd"
371378
yarn_node_repositories_entry = "bin/yarn_node_repositories" if not is_windows else "bin/yarn_node_repositories.cmd"
@@ -420,8 +427,8 @@ CALL "%SCRIPT_DIR%\\{node}" {args} %*
420427
# Immediately exit if any command fails.
421428
set -e
422429
# Generated by node_repositories.bzl
423-
export NODE_REPOSITORY_ARGS={}
424-
""".format(node_repo_args), executable = True)
430+
export NODE_REPOSITORY_ARGS={args}
431+
""".format(args = node_repo_args), executable = True)
425432

426433
# The entry points for npm for osx/linux and windows
427434
# Runs npm using appropriate node entry point
@@ -609,18 +616,29 @@ node_repositories_rule = repository_rule(
609616
)
610617

611618
def _nodejs_host_os_alias_impl(repository_ctx):
612-
host_os = os_name(repository_ctx)
613-
node_repository = "@nodejs_%s" % host_os
614619
is_windows_host = is_windows_os(repository_ctx)
615620
file_ending = ".cmd" if is_windows_host else ""
616-
actual_node_bin = "bin/nodejs/node.exe" if is_windows_host else "bin/nodejs/bin/node"
621+
node_repository = "@nodejs_%s" % os_name(repository_ctx)
622+
if repository_ctx.attr.vendored_node:
623+
node_bin_repository = "@%s" % repository_ctx.attr.vendored_node.workspace_name
624+
actual_node_bin = "/".join([f for f in [
625+
repository_ctx.attr.vendored_node.package,
626+
repository_ctx.attr.vendored_node.name,
627+
"bin/node" if not is_windows_host else "node.exe",
628+
] if f])
629+
else:
630+
node_bin_repository = node_repository
631+
actual_node_bin = "%s/%s" % (
632+
NODE_EXTRACT_DIR,
633+
"node.exe" if is_windows_host else "bin/node",
634+
)
617635
repository_ctx.template(
618636
"BUILD.bazel",
619637
Label("@build_bazel_rules_nodejs//internal/node:BUILD.nodejs_host_os_alias.tpl"),
620638
substitutions = {
621639
"TEMPLATE__npm_node_repositories": "%s//:bin/npm_node_repositories%s" % (node_repository, file_ending),
622640
"TEMPLATE__yarn_node_repositories": "%s//:bin/yarn_node_repositories%s" % (node_repository, file_ending),
623-
"TEMPLATE_actual_node_bin": "%s//:%s" % (node_repository, actual_node_bin),
641+
"TEMPLATE_actual_node_bin": "%s//:%s" % (node_bin_repository, actual_node_bin),
624642
"TEMPLATE_node_repo_args": "%s//:bin/node_repo_args.sh" % node_repository,
625643
"TEMPLATE_npm": "%s//:bin/npm%s" % (node_repository, file_ending),
626644
"TEMPLATE_run_npm": "%s//:run_npm.sh.template" % node_repository,
@@ -632,9 +650,12 @@ def _nodejs_host_os_alias_impl(repository_ctx):
632650

633651
_nodejs_repo_host_os_alias = repository_rule(
634652
_nodejs_host_os_alias_impl,
653+
attrs = {
654+
"vendored_node": attr.label(allow_single_file = True),
655+
},
635656
)
636657

637-
def node_repositories(package_json = [], **kwargs):
658+
def node_repositories(**kwargs):
638659
"""
639660
Wrapper macro around node_repositories_rule to call it for each platform, register bazel toolchains,
640661
and make other convenience repositories.
@@ -652,14 +673,16 @@ def node_repositories(package_json = [], **kwargs):
652673
minimum_bazel_version = "0.21.0",
653674
)
654675

676+
vendored_node = kwargs.pop("vendored_node", None)
677+
655678
# This needs to be setup so toolchains can access nodejs for all different versions
656679
for os_arch_name in OS_ARCH_NAMES:
657680
os_name = "_".join(os_arch_name)
658681
node_repository_name = "nodejs_%s" % os_name
659682
_maybe(
660683
node_repositories_rule,
661684
name = node_repository_name,
662-
package_json = package_json,
685+
vendored_node = vendored_node,
663686
**kwargs
664687
)
665688
native.register_toolchains("@build_bazel_rules_nodejs//toolchains/node:node_%s_toolchain" % os_arch_name[0])
@@ -673,6 +696,7 @@ def node_repositories(package_json = [], **kwargs):
673696
_maybe(
674697
_nodejs_repo_host_os_alias,
675698
name = "nodejs",
699+
vendored_node = vendored_node,
676700
)
677701

678702
_maybe(

internal/npm_package/npm_package.bzl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ to the `deps` of one of their targets.
77
"""
88

99
load("@build_bazel_rules_nodejs//:providers.bzl", "DeclarationInfo", "JSNamedModuleInfo")
10+
load("//internal/common:path_utils.bzl", "strip_external")
1011

1112
# Takes a depset of files and returns a corresponding list of file paths without any files
1213
# that aren't part of the specified package path. Also include files from external repositories
@@ -65,7 +66,7 @@ def create_package(ctx, deps_sources, nested_packages):
6566
args.add("1" if ctx.attr.rename_build_files else "0")
6667

6768
# require.resolve expects the path to start with the workspace name and not "external"
68-
run_npm_template_path = ctx.file._run_npm_template.path[len("external") + 1:] if ctx.file._run_npm_template.path.startswith("external") else ctx.file._run_npm_template.path
69+
run_npm_template_path = strip_external(ctx.file._run_npm_template.path)
6970
args.add(run_npm_template_path)
7071

7172
inputs = ctx.files.srcs + deps_sources + nested_packages + [ctx.file._run_npm_template]

0 commit comments

Comments
 (0)