Skip to content

Commit 20f4a8f

Browse files
committed
feat(builtin): add a toolchain to new core that exposes the node for any platform
1 parent a32cf5c commit 20f4a8f

File tree

8 files changed

+374
-72
lines changed

8 files changed

+374
-72
lines changed

e2e/core/BUILD.bazel

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,80 @@
11
load("@bazel_skylib//rules:write_file.bzl", "write_file")
22
load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
3+
load(":defs.bzl", "my_nodejs")
34

5+
# Trivial test fixture: a nodejs program that writes to a file
46
write_file(
57
name = "js",
68
out = "some.js",
79
content = ["require('fs').writeFileSync(process.argv[2], 'stuff')"],
810
)
911

10-
# Temporary fixture until we have toolchains hooked up in the core package
11-
alias(
12-
name = "node_bin",
13-
actual = select({
14-
"@bazel_tools//src/conditions:darwin_arm64": "@node16_darwin_arm64//:node_bin",
15-
"@bazel_tools//src/conditions:darwin_x86_64": "@node16_darwin_amd64//:node_bin",
16-
"@bazel_tools//src/conditions:linux_aarch64": "@node16_linux_arm64//:node_bin",
17-
"@bazel_tools//src/conditions:linux_s390x": "@node16_linux_s390x//:node_bin",
18-
"@bazel_tools//src/conditions:linux_x86_64": "@node16_linux_amd64//:node_bin",
19-
"@bazel_tools//src/conditions:linux_ppc64le": "@node16_linux_ppc64le//:node_bin",
20-
"@bazel_tools//src/conditions:windows": "@node16_windows_amd64//:node_bin",
21-
"//conditions:default": "@node16_linux_amd64//:node_bin",
22-
}),
23-
)
12+
# This technique can be used to directly grab a node binary as a label, however it has the
13+
# downside that analysis phase on this select() statement will cause an eager fetch of all
14+
# the platforms and therefore download a bunch of node binaries.
15+
# This is what toolchains solves, so we don't recommend doing this.
16+
# alias(
17+
# name = "node_bin",
18+
# actual = select({
19+
# "@bazel_tools//src/conditions:darwin_arm64": "@node16_darwin_arm64//:node_bin",
20+
# "@bazel_tools//src/conditions:darwin_x86_64": "@node16_darwin_amd64//:node_bin",
21+
# "@bazel_tools//src/conditions:linux_aarch64": "@node16_linux_arm64//:node_bin",
22+
# "@bazel_tools//src/conditions:linux_s390x": "@node16_linux_s390x//:node_bin",
23+
# "@bazel_tools//src/conditions:linux_x86_64": "@node16_linux_amd64//:node_bin",
24+
# "@bazel_tools//src/conditions:linux_ppc64le": "@node16_linux_ppc64le//:node_bin",
25+
# "@bazel_tools//src/conditions:windows": "@node16_windows_amd64//:node_bin",
26+
# "//conditions:default": "@node16_linux_amd64//:node_bin",
27+
# }),
28+
# )
29+
# genrule(
30+
# name = "use_node_bin",
31+
# srcs = ["some.js"],
32+
# outs = ["thing1"],
33+
# cmd = "$(execpath :node_bin) $(execpath some.js) $@",
34+
# tools = [":node_bin"],
35+
# )
36+
37+
# In theory, you can use the node toolchain together with a genrule().
38+
# However the genrule implementation doesn't perform toolchain resolution.
39+
# See this comment from Jay Conrod about a similar question for rules_go
40+
# https://github.com/bazelbuild/rules_go/issues/2255#issuecomment-545478712
41+
# That means you must resolve the toolchain yourself, with a select()
42+
# and that falls down the same deoptimization described above:
43+
# it will eager-fetch node for all platforms.
44+
# So instead we recommend always writing a custom rule to access the node binary.
45+
# alias(
46+
# name = "node_toolchain",
47+
# actual = select({
48+
# "@bazel_tools//src/conditions:darwin_arm64": "@node16_darwin_arm64//:node_toolchain",
49+
# "@bazel_tools//src/conditions:darwin_x86_64": "@node16_darwin_amd64//:node_toolchain",
50+
# "@bazel_tools//src/conditions:linux_aarch64": "@node16_linux_arm64//:node_toolchain",
51+
# "@bazel_tools//src/conditions:linux_s390x": "@node16_linux_s390x//:node_toolchain",
52+
# "@bazel_tools//src/conditions:linux_x86_64": "@node16_linux_amd64//:node_toolchain",
53+
# "@bazel_tools//src/conditions:linux_ppc64le": "@node16_linux_ppc64le//:node_toolchain",
54+
# "@bazel_tools//src/conditions:windows": "@node16_windows_amd64//:node_toolchain",
55+
# "//conditions:default": "@node16_linux_amd64//:node_toolchain",
56+
# }),
57+
# )
58+
# genrule(
59+
# name = "use_node_toolchain",
60+
# srcs = ["some.js"],
61+
# outs = ["thing2"],
62+
# cmd = "$(NODE_PATH) $(execpath some.js) $@",
63+
# toolchains = [":node_toolchain"],
64+
# # It will also fail to include the files from the node_toolchain, so you're
65+
# # forced to repeat the label of the node binary as an explicit input.
66+
# tools = ["@node16_host//:node_bin"],
67+
# )
2468

25-
genrule(
26-
name = "try",
27-
srcs = ["some.js"],
28-
outs = ["thing"],
29-
cmd = "$(execpath :node_bin) $(execpath some.js) $@",
30-
tools = [":node_bin"],
69+
# Here, my_nodejs is a fake for something like nodejs_binary or
70+
# some other custom rule that runs node.
71+
my_nodejs(
72+
name = "run",
73+
out = "thing",
74+
entry_point = "some.js",
3175
)
3276

77+
# Assert that the node program wrote the file we expect
3378
write_file(
3479
name = "write_expected",
3580
out = "expected",
@@ -41,3 +86,4 @@ diff_test(
4186
file1 = "expected",
4287
file2 = "thing",
4388
)
89+
# end Assert

e2e/core/defs.bzl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"Simple rule to test nodejs toolchain"
2+
3+
def _my_nodejs_impl(ctx):
4+
toolchain = ctx.toolchains["@rules_nodejs//nodejs:toolchain_type"].nodeinfo
5+
ctx.actions.run(
6+
inputs = toolchain.tool_files + [ctx.file.entry_point],
7+
executable = toolchain.target_tool_path,
8+
arguments = [ctx.file.entry_point.path, ctx.outputs.out.path],
9+
outputs = [ctx.outputs.out],
10+
)
11+
return []
12+
13+
my_nodejs = rule(
14+
implementation = _my_nodejs_impl,
15+
attrs = {
16+
"entry_point": attr.label(allow_single_file = True),
17+
"out": attr.output(),
18+
},
19+
toolchains = ["@rules_nodejs//nodejs:toolchain_type"],
20+
)

internal/node/node_repositories.bzl

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,45 +19,10 @@ See https://docs.bazel.build/versions/main/skylark/repository_rules.html
1919
"""
2020

2121
load("//internal/common:check_bazel_version.bzl", "check_bazel_version")
22-
load("//nodejs/private:os_name.bzl", "OS_ARCH_NAMES", "assert_node_exists_for_host", "node_exists_for_os", "os_name")
23-
load("//toolchains/node:node_toolchain_configure.bzl", "node_toolchain_configure")
22+
load("//nodejs/private:nodejs_repo_host_os_alias.bzl", "nodejs_repo_host_os_alias")
23+
load("//nodejs/private:os_name.bzl", "OS_ARCH_NAMES", "node_exists_for_os", "os_name")
2424
load("//nodejs:repositories.bzl", "DEFAULT_NODE_VERSION", node_repositories_rule = "node_repositories")
25-
26-
def _nodejs_host_os_alias_impl(repository_ctx):
27-
assert_node_exists_for_host(repository_ctx)
28-
29-
# Base BUILD file for this repository
30-
repository_ctx.file("BUILD.bazel", content = """# Generated by node_repositories.bzl
31-
package(default_visibility = ["//visibility:public"])
32-
# aliases for exports_files
33-
alias(name = "run_npm.sh.template", actual = "{node_repository}//:run_npm.sh.template")
34-
alias(name = "run_npm.bat.template", actual = "{node_repository}//:run_npm.bat.template")
35-
alias(name = "bin/node_repo_args.sh", actual = "{node_repository}//:bin/node_repo_args.sh")
36-
# aliases for other aliases
37-
alias(name = "node_bin", actual = "{node_repository}//:node_bin")
38-
alias(name = "npm_bin", actual = "{node_repository}//:npm_bin")
39-
alias(name = "npx_bin", actual = "{node_repository}//:npx_bin")
40-
alias(name = "yarn_bin", actual = "{node_repository}//:yarn_bin")
41-
alias(name = "node", actual = "{node_repository}//:node")
42-
alias(name = "npm", actual = "{node_repository}//:npm")
43-
alias(name = "yarn", actual = "{node_repository}//:yarn")
44-
alias(name = "npm_node_repositories", actual = "{node_repository}//:npm_node_repositories")
45-
alias(name = "yarn_node_repositories", actual = "{node_repository}//:yarn_node_repositories")
46-
alias(name = "node_files", actual = "{node_repository}//:node_files")
47-
alias(name = "yarn_files", actual = "{node_repository}//:yarn_files")
48-
alias(name = "npm_files", actual = "{node_repository}//:npm_files")
49-
exports_files(["index.bzl"])
50-
""".format(node_repository = "@nodejs_%s" % os_name(repository_ctx)))
51-
52-
# index.bzl file for this repository
53-
repository_ctx.file("index.bzl", content = """# Generated by node_repositories.bzl
54-
host_platform="{host_platform}"
55-
""".format(host_platform = os_name(repository_ctx)))
56-
57-
_nodejs_repo_host_os_alias = repository_rule(
58-
_nodejs_host_os_alias_impl,
59-
attrs = {"node_version": attr.string()},
60-
)
25+
load("//toolchains/node:node_toolchain_configure.bzl", "node_toolchain_configure")
6126

6227
def node_repositories(**kwargs):
6328
"""
@@ -101,7 +66,7 @@ def node_repositories(**kwargs):
10166
# This "nodejs" repo is just for convenience so one does not have to target @nodejs_<os_name>//...
10267
# All it does is create aliases to the @nodejs_<host_os>_<host_arch> repository
10368
_maybe(
104-
_nodejs_repo_host_os_alias,
69+
nodejs_repo_host_os_alias,
10570
name = "nodejs",
10671
node_version = node_version,
10772
)

nodejs/BUILD.bazel

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,12 @@ bzl_library(
1515
"//third_party/github.com/bazelbuild/bazel-skylib:bzl",
1616
],
1717
)
18+
19+
# This is the target rule authors should put in their "toolchains"
20+
# attribute in order to get a node interpreter for the correct
21+
# platform.
22+
# See https://docs.bazel.build/versions/main/toolchains.html#writing-rules-that-use-toolchains
23+
toolchain_type(
24+
name = "toolchain_type",
25+
visibility = ["//visibility:public"],
26+
)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"Provide convenience repository for the host platform like @nodejs"
2+
3+
load("//nodejs/private:os_name.bzl", "os_name")
4+
5+
def _nodejs_host_os_alias_impl(repository_ctx):
6+
# Base BUILD file for this repository
7+
repository_ctx.file("BUILD.bazel", """# Generated by nodejs_repo_host_os_alias.bzl
8+
package(default_visibility = ["//visibility:public"])
9+
# aliases for exports_files
10+
alias(name = "run_npm.sh.template", actual = "@{node_repository}_{os_name}//:run_npm.sh.template")
11+
alias(name = "run_npm.bat.template", actual = "@{node_repository}_{os_name}//:run_npm.bat.template")
12+
alias(name = "bin/node_repo_args.sh", actual = "@{node_repository}_{os_name}//:bin/node_repo_args.sh")
13+
14+
# aliases for other aliases
15+
alias(name = "node_bin", actual = "@{node_repository}_{os_name}//:node_bin")
16+
alias(name = "npm_bin", actual = "@{node_repository}_{os_name}//:npm_bin")
17+
alias(name = "npx_bin", actual = "@{node_repository}_{os_name}//:npx_bin")
18+
alias(name = "yarn_bin", actual = "@{node_repository}_{os_name}//:yarn_bin")
19+
alias(name = "node", actual = "@{node_repository}_{os_name}//:node")
20+
alias(name = "npm", actual = "@{node_repository}_{os_name}//:npm")
21+
alias(name = "yarn", actual = "@{node_repository}_{os_name}//:yarn")
22+
alias(name = "npm_node_repositories", actual = "@{node_repository}_{os_name}//:npm_node_repositories")
23+
alias(name = "yarn_node_repositories", actual = "@{node_repository}_{os_name}//:yarn_node_repositories")
24+
alias(name = "node_files", actual = "@{node_repository}_{os_name}//:node_files")
25+
alias(name = "yarn_files", actual = "@{node_repository}_{os_name}//:yarn_files")
26+
alias(name = "npm_files", actual = "@{node_repository}_{os_name}//:npm_files")
27+
exports_files(["index.bzl"])
28+
""".format(
29+
node_repository = repository_ctx.attr.user_node_repository_name,
30+
os_name = os_name(repository_ctx),
31+
))
32+
33+
# index.bzl file for this repository
34+
repository_ctx.file("index.bzl", content = """# Generated by nodejs_repo_host_os_alias.bzl
35+
host_platform="{host_platform}"
36+
""".format(host_platform = os_name(repository_ctx)))
37+
38+
nodejs_repo_host_os_alias = repository_rule(
39+
_nodejs_host_os_alias_impl,
40+
doc = """Creates a repository with a shorter name meant for the host platform, which contains
41+
42+
- A BUILD.bazel file declaring aliases to the host platform's node binaries
43+
- index.bzl containing some constants
44+
""",
45+
attrs = {
46+
"user_node_repository_name": attr.string(
47+
default = "nodejs",
48+
doc = "User-provided name from the workspace file, eg. node16",
49+
),
50+
# FIXME: this seems unused, but not the time to make that edit right now
51+
"node_version": attr.string(),
52+
},
53+
)

nodejs/private/toolchains_repo.bzl

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Create a repository to hold the toolchains
2+
3+
This follows guidance here:
4+
https://docs.bazel.build/versions/main/skylark/deploying.html#registering-toolchains
5+
"
6+
Note that in order to resolve toolchains in the analysis phase
7+
Bazel needs to analyze all toolchain targets that are registered.
8+
Bazel will not need to analyze all targets referenced by toolchain.toolchain attribute.
9+
If in order to register toolchains you need to perform complex computation in the repository,
10+
consider splitting the repository with toolchain targets
11+
from the repository with <LANG>_toolchain targets.
12+
Former will be always fetched,
13+
and the latter will only be fetched when user actually needs to build <LANG> code.
14+
"
15+
The "complex computation" in our case is simply downloading the node binaries from nodejs.org.
16+
This guidance tells us how to avoid that: we put the toolchain targets in the alias repository
17+
with only the toolchain attribute pointing into the platform-specific repositories.
18+
"""
19+
20+
PLATFORMS = {
21+
"darwin_amd64": struct(
22+
compatible_with = [
23+
"@platforms//os:macos",
24+
"@platforms//cpu:x86_64",
25+
],
26+
),
27+
"darwin_arm64": struct(
28+
compatible_with = [
29+
"@platforms//os:macos",
30+
"@platforms//cpu:aarch64",
31+
],
32+
),
33+
"linux_amd64": struct(
34+
compatible_with = [
35+
"@platforms//os:linux",
36+
"@platforms//cpu:x86_64",
37+
],
38+
),
39+
"linux_arm64": struct(
40+
compatible_with = [
41+
"@platforms//os:linux",
42+
"@platforms//cpu:aarch64",
43+
],
44+
),
45+
"windows_amd64": struct(
46+
compatible_with = [
47+
"@platforms//os:windows",
48+
"@platforms//cpu:x86_64",
49+
],
50+
),
51+
"linux_s390x": struct(
52+
compatible_with = [
53+
"@platforms//os:linux",
54+
"@platforms//cpu:s390x",
55+
],
56+
),
57+
"linux_ppc64le": struct(
58+
compatible_with = [
59+
"@platforms//os:linux",
60+
"@platforms//cpu:ppc",
61+
],
62+
),
63+
}
64+
65+
def _impl(repository_ctx):
66+
build_content = """# Generated by toolchains_repo.bzl
67+
#
68+
# These can be registered in the workspace file or passed to --extra_toolchains flag.
69+
# By default all these toolchains are registered by the nodejs_register_toolchains macro
70+
# so you don't normally need to interact with these targets.
71+
"""
72+
73+
for [platform, meta] in PLATFORMS.items():
74+
build_content += """
75+
toolchain(
76+
name = "{platform}_toolchain",
77+
exec_compatible_with = {compatible_with},
78+
target_compatible_with = {compatible_with},
79+
toolchain = "@{user_node_repository_name}_{platform}//:node_toolchain",
80+
toolchain_type = "@rules_nodejs//nodejs:toolchain_type",
81+
)
82+
""".format(
83+
platform = platform,
84+
name = repository_ctx.attr.name,
85+
user_node_repository_name = repository_ctx.attr.user_node_repository_name,
86+
compatible_with = meta.compatible_with,
87+
)
88+
89+
# Base BUILD file for this repository
90+
repository_ctx.file("BUILD.bazel", build_content)
91+
92+
toolchains_repo = repository_rule(
93+
_impl,
94+
doc = """Creates a repository with toolchain definitions for all known platforms
95+
which can be registered or selected.""",
96+
attrs = {
97+
"user_node_repository_name": attr.string(doc = "what the user chose for the base name, eg. node16"),
98+
},
99+
)

0 commit comments

Comments
 (0)