From ff1c15b3704d7edaaf7f25935f17ad9a2d993daa Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 11 Jun 2026 16:52:46 +0200 Subject: [PATCH 1/6] feat: use facts to auto-retrieve node versions if possible --- e2e/nodejs_host/BUILD.bazel | 3 + e2e/nodejs_host/MODULE.bazel | 11 ++++ nodejs/BUILD.bazel | 7 ++- nodejs/extensions.bzl | 66 +++++++++++++++++++++- nodejs/private/BUILD.bazel | 10 ++++ nodejs/private/fetch_node_repositories.bzl | 51 +++++++++++++++++ nodejs/private/version_from_attr.bzl | 27 +++++++++ nodejs/repositories.bzl | 13 +---- 8 files changed, 175 insertions(+), 13 deletions(-) create mode 100644 nodejs/private/fetch_node_repositories.bzl create mode 100644 nodejs/private/version_from_attr.bzl diff --git a/e2e/nodejs_host/BUILD.bazel b/e2e/nodejs_host/BUILD.bazel index 1871163ff4..3ca45b6354 100644 --- a/e2e/nodejs_host/BUILD.bazel +++ b/e2e/nodejs_host/BUILD.bazel @@ -23,6 +23,7 @@ load("@rules_shell//shell:sh_test.bzl", "sh_test") "node16", "node16_nvmrc", "node17_custom", + "node26_facts", ] ] @@ -66,5 +67,7 @@ load("@rules_shell//shell:sh_test.bzl", "sh_test") ("node16_nvmrc", "npx"), ("node17_custom", "npm"), ("node17_custom", "npx"), + ("node26_facts", "npm"), + ("node26_facts", "npx"), ] ] diff --git a/e2e/nodejs_host/MODULE.bazel b/e2e/nodejs_host/MODULE.bazel index 8c2b4ffc83..f68601d80c 100644 --- a/e2e/nodejs_host/MODULE.bazel +++ b/e2e/nodejs_host/MODULE.bazel @@ -35,6 +35,10 @@ node.toolchain( node_urls = ["https://nodejs.org/dist/v17.0.1/{filename}"], node_version = "17.0.1.custom", ) +node.toolchain( + name = "node26_facts", + node_version = "26.3.0", +) # FIXME(6.0): a repo rule with name=foo should create a repo named @foo, not @foo_toolchains use_repo( @@ -60,6 +64,13 @@ use_repo( "node17_custom_linux_arm64", "node17_custom_toolchains", "node17_custom_windows_amd64", + "node26_facts", + "node26_facts_darwin_amd64", + "node26_facts_darwin_arm64", + "node26_facts_linux_amd64", + "node26_facts_linux_arm64", + "node26_facts_toolchains", + "node26_facts_windows_amd64", "nodejs", "nodejs_darwin_amd64", "nodejs_darwin_arm64", diff --git a/nodejs/BUILD.bazel b/nodejs/BUILD.bazel index 622b1f771c..d1d9c9a8c5 100644 --- a/nodejs/BUILD.bazel +++ b/nodejs/BUILD.bazel @@ -9,7 +9,11 @@ package(default_visibility = ["//visibility:public"]) bzl_library( name = "extensions", srcs = ["extensions.bzl"], - deps = [":repositories"], + deps = [ + ":repositories", + "//nodejs/private:fetch_node_repositories", + "//nodejs/private:version_from_attr", + ], ) bzl_library( @@ -29,6 +33,7 @@ bzl_library( "//nodejs/private:nodejs_repo_host_os_alias", "//nodejs/private:nodejs_toolchains_repo", "//nodejs/private:os_name", + "//nodejs/private:version_from_attr", ], ) diff --git a/nodejs/extensions.bzl b/nodejs/extensions.bzl index 3b4b53f10c..2e85b5ab48 100644 --- a/nodejs/extensions.bzl +++ b/nodejs/extensions.bzl @@ -9,6 +9,9 @@ use_repo(node, "nodejs_toolchains") ``` """ +load("//nodejs/private:fetch_node_repositories.bzl", "fetch_node_repositories") +load("//nodejs/private:nodejs_toolchains_repo.bzl", "PLATFORMS") +load("//nodejs/private:version_from_attr.bzl", "version_from_attr") load( ":repositories.bzl", "DEFAULT_NODE_REPOSITORY", @@ -29,6 +32,57 @@ def _toolchains_equal(lhs, rhs): return False return True +def _use_repository_facts(registration): + """Whether we should use facts (if available) for the given registration.""" + + if registration.node_repositories.items(): + return False # custom repositories, do not mess with them. + if registration.node_urls != [DEFAULT_NODE_URL]: + return False # we do not know how to fetch shas with custom URLs. + + return True + +def _update_repository_facts(module_ctx, registrations): + """Fetch / update the necessary repository facts for the given registrations. + + * Takes into account existing facts. + * Returns empty dict if facts are not supported. + """ + + if not hasattr(module_ctx, "facts"): + # facts not supported. + # repository rules will fallback to builtin NODE_VERSIONS. + return {} + + new_facts = {} + + for registration in registrations.values(): + if not _use_repository_facts(registration): + continue + + version = version_from_attr(module_ctx, registration) + + should_fetch = False + + # Get repository values for the default PLATFORMS. + # These are the only platforms supported by the extension anyways. + for platform in PLATFORMS: + key = version + "-" + platform + + existing = new_facts.get(key) or module_ctx.facts.get(key) + if existing: + new_facts[key] = existing + else: + should_fetch = True + + if should_fetch: + # Note: Even after fetching, it is possible that keys are not in facts: + # Old node versions might not have all platforms. + # In that case, we want to fail lazily when the repository is actually used. + new_facts.update(fetch_node_repositories(module_ctx, version)) + + return new_facts + def _toolchain_extension(module_ctx): registrations = {} for mod in module_ctx.modules: @@ -52,17 +106,27 @@ def _toolchain_extension(module_ctx): else: registrations[toolchain.name] = toolchain + repository_facts = _update_repository_facts(module_ctx, registrations) + for k, v in registrations.items(): nodejs_register_toolchains( name = k, node_version = v.node_version, node_version_from_nvmrc = v.node_version_from_nvmrc, node_urls = v.node_urls, - node_repositories = v.node_repositories, + node_repositories = ( + repository_facts if _use_repository_facts(v) else v.node_repositories + ), include_headers = v.include_headers, register = False, ) + if hasattr(module_ctx, "facts"): + return module_ctx.extension_metadata( + reproducible = True, + facts = repository_facts, + ) + _ATTRS = { "name": attr.string( doc = "Base name for generated repositories", diff --git a/nodejs/private/BUILD.bazel b/nodejs/private/BUILD.bazel index 7388dc4f8b..9c92ac714f 100644 --- a/nodejs/private/BUILD.bazel +++ b/nodejs/private/BUILD.bazel @@ -41,3 +41,13 @@ bzl_library( srcs = ["user_build_settings.bzl"], deps = ["//nodejs/private/providers:user_build_settings"], ) + +bzl_library( + name = "version_from_attr", + srcs = ["version_from_attr.bzl"], +) + +bzl_library( + name = "fetch_node_repositories", + srcs = ["fetch_node_repositories.bzl"], +) diff --git a/nodejs/private/fetch_node_repositories.bzl b/nodejs/private/fetch_node_repositories.bzl new file mode 100644 index 0000000000..48c7e98c75 --- /dev/null +++ b/nodejs/private/fetch_node_repositories.bzl @@ -0,0 +1,51 @@ +"""Implementation of node SHASUM fetching for facts.""" + +_REPOSITORY_TYPES = { + "darwin-arm64.tar.gz": "darwin_arm64", + "darwin-x64.tar.gz": "darwin_amd64", + "linux-x64.tar.xz": "linux_amd64", + "linux-arm64.tar.xz": "linux_arm64", + "linux-s390x.tar.xz": "linux_s390x", + "win-x64.zip": "windows_amd64", + "win-arm64.zip": "windows_arm64", + "linux-ppc64le.tar.xz": "linux_ppc64le", +} + +def fetch_node_repositories(module_ctx, version): + """Fetches node repositories for the given node version. + + Port of scripts/update-nodejs-versions.js + """ + + shasums_filename = "{version}-SHASUMS256.txt".format(version = version) + url = "https://nodejs.org/dist/v{version}/SHASUMS256.txt".format(version = version) + + result = module_ctx.download(url = url, output = shasums_filename) + if not result.success: + fail("Failed to fetch node shasums:", url, result, sep = "\n") + + shasums = module_ctx.read(shasums_filename) + + result = {} + + for line in shasums.splitlines(): + line = line.strip() + if not line: + continue + + parts = line.split(" ") + if len(parts) != 2: + fail("{url} contains unexpected line:\n{line}".format( + url = url, + line = line, + )) + + sha, filename = parts + type = _REPOSITORY_TYPES.get(filename.removeprefix("node-v%s-" % version)) + if not type: + continue + + strip_prefix = filename.removesuffix(".tar.gz").removesuffix(".tar.xz").removesuffix(".zip") + result[version + "-" + type] = (filename, strip_prefix, sha) + + return result diff --git a/nodejs/private/version_from_attr.bzl b/nodejs/private/version_from_attr.bzl new file mode 100644 index 0000000000..47b6cd1f9a --- /dev/null +++ b/nodejs/private/version_from_attr.bzl @@ -0,0 +1,27 @@ +"""Helper to get node version from user config.""" + +def _verify_version_is_valid(version): + major, minor, patch = (version.split(".") + [None, None, None])[:3] + if not major.isdigit() or not minor.isdigit() or not patch.isdigit(): + fail("Invalid node version: %s" % version) + +def version_from_attr(ctx, attr): + """Extract the node version from attr. + + Verifies if the extracted version is valid. + + Args: + ctx: repository or module context + attr: A struct with fields node_version and node_version_from_nvmrc + + Returns: node version. + """ + + node_version = attr.node_version + + if attr.node_version_from_nvmrc: + node_version = str(ctx.read(attr.node_version_from_nvmrc)).strip() + + _verify_version_is_valid(node_version) + + return node_version diff --git a/nodejs/repositories.bzl b/nodejs/repositories.bzl index 3a278cf0bd..d5f7ab29ce 100644 --- a/nodejs/repositories.bzl +++ b/nodejs/repositories.bzl @@ -3,6 +3,7 @@ load("//nodejs/private:node_versions.bzl", "NODE_VERSIONS") load("//nodejs/private:nodejs_repo_host_os_alias.bzl", "nodejs_repo_host_os_alias") load("//nodejs/private:nodejs_toolchains_repo.bzl", "PLATFORMS", "nodejs_toolchains_repo") +load("//nodejs/private:version_from_attr.bzl", "version_from_attr") # Default base name for node toolchain repositories # created by the module extension @@ -66,12 +67,7 @@ def _download_node(repository_ctx): # @nodejs_PLATFORM where PLATFORM is one of BUILT_IN_NODE_PLATFORMS host_os = repository_ctx.attr.platform or repository_ctx.name.split("nodejs_", 1)[1] - node_version = repository_ctx.attr.node_version - - if repository_ctx.attr.node_version_from_nvmrc: - node_version = str(repository_ctx.read(repository_ctx.attr.node_version_from_nvmrc)).strip() - - _verify_version_is_valid(node_version) + node_version = version_from_attr(repository_ctx, repository_ctx.attr) node_repositories = repository_ctx.attr.node_repositories @@ -302,11 +298,6 @@ def _strip_bin(path): return path[len("bin/"):] -def _verify_version_is_valid(version): - major, minor, patch = (version.split(".") + [None, None, None])[:3] - if not major.isdigit() or not minor.isdigit() or not patch.isdigit(): - fail("Invalid node version: %s" % version) - def _nodejs_repositories_impl(repository_ctx): reproducible = _download_node(repository_ctx) _prepare_node(repository_ctx) From 1e4ce48fa3fe8e2329a06a05c549e7531e8ac907 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 12 Jun 2026 13:13:38 +0200 Subject: [PATCH 2/6] Buildifier fixes --- nodejs/extensions.bzl | 2 ++ nodejs/private/fetch_node_repositories.bzl | 7 +++++++ nodejs/private/version_from_attr.bzl | 3 ++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/nodejs/extensions.bzl b/nodejs/extensions.bzl index 2e85b5ab48..4a4af9bbbb 100644 --- a/nodejs/extensions.bzl +++ b/nodejs/extensions.bzl @@ -126,6 +126,8 @@ def _toolchain_extension(module_ctx): reproducible = True, facts = repository_facts, ) + else: + return # buildifier: disable=return-value (allow no value) _ATTRS = { "name": attr.string( diff --git a/nodejs/private/fetch_node_repositories.bzl b/nodejs/private/fetch_node_repositories.bzl index 48c7e98c75..ab0627aaf9 100644 --- a/nodejs/private/fetch_node_repositories.bzl +++ b/nodejs/private/fetch_node_repositories.bzl @@ -15,6 +15,13 @@ def fetch_node_repositories(module_ctx, version): """Fetches node repositories for the given node version. Port of scripts/update-nodejs-versions.js + + Args: + module_ctx: Module context + version: The node version to fetch repositories for. + + Returns: + A dictionary in the shape of node_repositories. """ shasums_filename = "{version}-SHASUMS256.txt".format(version = version) diff --git a/nodejs/private/version_from_attr.bzl b/nodejs/private/version_from_attr.bzl index 47b6cd1f9a..6d7fa20655 100644 --- a/nodejs/private/version_from_attr.bzl +++ b/nodejs/private/version_from_attr.bzl @@ -14,7 +14,8 @@ def version_from_attr(ctx, attr): ctx: repository or module context attr: A struct with fields node_version and node_version_from_nvmrc - Returns: node version. + Returns: + The node version. """ node_version = attr.node_version From 3d4d2f5982908c1de5b3cf88a02c584356918ea1 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Mon, 15 Jun 2026 11:03:07 +0200 Subject: [PATCH 3/6] Only use facts for unknown node versions --- nodejs/BUILD.bazel | 1 + nodejs/extensions.bzl | 79 +++++++++++-------------------------------- 2 files changed, 20 insertions(+), 60 deletions(-) diff --git a/nodejs/BUILD.bazel b/nodejs/BUILD.bazel index d1d9c9a8c5..aa8cf9a1c2 100644 --- a/nodejs/BUILD.bazel +++ b/nodejs/BUILD.bazel @@ -12,6 +12,7 @@ bzl_library( deps = [ ":repositories", "//nodejs/private:fetch_node_repositories", + "//nodejs/private:node_versions", "//nodejs/private:version_from_attr", ], ) diff --git a/nodejs/extensions.bzl b/nodejs/extensions.bzl index 4a4af9bbbb..496023933a 100644 --- a/nodejs/extensions.bzl +++ b/nodejs/extensions.bzl @@ -10,7 +10,7 @@ use_repo(node, "nodejs_toolchains") """ load("//nodejs/private:fetch_node_repositories.bzl", "fetch_node_repositories") -load("//nodejs/private:nodejs_toolchains_repo.bzl", "PLATFORMS") +load("//nodejs/private:node_versions.bzl", "NODE_VERSIONS") load("//nodejs/private:version_from_attr.bzl", "version_from_attr") load( ":repositories.bzl", @@ -32,57 +32,6 @@ def _toolchains_equal(lhs, rhs): return False return True -def _use_repository_facts(registration): - """Whether we should use facts (if available) for the given registration.""" - - if registration.node_repositories.items(): - return False # custom repositories, do not mess with them. - if registration.node_urls != [DEFAULT_NODE_URL]: - return False # we do not know how to fetch shas with custom URLs. - - return True - -def _update_repository_facts(module_ctx, registrations): - """Fetch / update the necessary repository facts for the given registrations. - - * Takes into account existing facts. - * Returns empty dict if facts are not supported. - """ - - if not hasattr(module_ctx, "facts"): - # facts not supported. - # repository rules will fallback to builtin NODE_VERSIONS. - return {} - - new_facts = {} - - for registration in registrations.values(): - if not _use_repository_facts(registration): - continue - - version = version_from_attr(module_ctx, registration) - - should_fetch = False - - # Get repository values for the default PLATFORMS. - # These are the only platforms supported by the extension anyways. - for platform in PLATFORMS: - key = version + "-" + platform - - existing = new_facts.get(key) or module_ctx.facts.get(key) - if existing: - new_facts[key] = existing - else: - should_fetch = True - - if should_fetch: - # Note: Even after fetching, it is possible that keys are not in facts: - # Old node versions might not have all platforms. - # In that case, we want to fail lazily when the repository is actually used. - new_facts.update(fetch_node_repositories(module_ctx, version)) - - return new_facts - def _toolchain_extension(module_ctx): registrations = {} for mod in module_ctx.modules: @@ -106,25 +55,35 @@ def _toolchain_extension(module_ctx): else: registrations[toolchain.name] = toolchain - repository_facts = _update_repository_facts(module_ctx, registrations) + supports_facts = hasattr(module_ctx, "facts") + new_repository_facts = {} for k, v in registrations.items(): + node_version = version_from_attr(module_ctx, v) + node_repositories = v.node_repositories or NODE_VERSIONS.get(node_version, {}) + + if supports_facts and not node_repositories: + node_repositories = ( + new_repository_facts.get(node_version) or + module_ctx.facts.get(node_version) or + # TODO: Add support for node_urls? + fetch_node_repositories(module_ctx, node_version) + ) + new_repository_facts[node_version] = node_repositories + nodejs_register_toolchains( name = k, - node_version = v.node_version, - node_version_from_nvmrc = v.node_version_from_nvmrc, + node_version = node_version, node_urls = v.node_urls, - node_repositories = ( - repository_facts if _use_repository_facts(v) else v.node_repositories - ), + node_repositories = node_repositories, include_headers = v.include_headers, register = False, ) - if hasattr(module_ctx, "facts"): + if supports_facts: return module_ctx.extension_metadata( reproducible = True, - facts = repository_facts, + facts = new_repository_facts, ) else: return # buildifier: disable=return-value (allow no value) From 7a66f738a427d82bc39c078f4688c28a0ce690e9 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Mon, 15 Jun 2026 10:44:53 +0200 Subject: [PATCH 4/6] Fix tests (using git_override of bazel_features) --- e2e/nodejs_host/BUILD.bazel | 15 ++++++++++----- e2e/nodejs_host/MODULE.bazel | 7 +++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/e2e/nodejs_host/BUILD.bazel b/e2e/nodejs_host/BUILD.bazel index 3ca45b6354..eda9a6a679 100644 --- a/e2e/nodejs_host/BUILD.bazel +++ b/e2e/nodejs_host/BUILD.bazel @@ -1,3 +1,4 @@ +load("@bazel_features//:features.bzl", "bazel_features") load("@rules_shell//shell:sh_test.bzl", "sh_test") # Dummy test target that imports various targets from the @@ -23,8 +24,9 @@ load("@rules_shell//shell:sh_test.bzl", "sh_test") "node16", "node16_nvmrc", "node17_custom", - "node26_facts", - ] + ] + ( + ["node26_facts"] if bazel_features.external_deps.has_facts else [] + ) ] [ @@ -67,7 +69,10 @@ load("@rules_shell//shell:sh_test.bzl", "sh_test") ("node16_nvmrc", "npx"), ("node17_custom", "npm"), ("node17_custom", "npx"), - ("node26_facts", "npm"), - ("node26_facts", "npx"), - ] + ] + ( + [ + ("node26_facts", "npm"), + ("node26_facts", "npx"), + ] if bazel_features.external_deps.has_facts else [] + ) ] diff --git a/e2e/nodejs_host/MODULE.bazel b/e2e/nodejs_host/MODULE.bazel index f68601d80c..23f039d3d2 100644 --- a/e2e/nodejs_host/MODULE.bazel +++ b/e2e/nodejs_host/MODULE.bazel @@ -6,8 +6,15 @@ local_path_override( bazel_dep(name = "bazel_lib", version = "3.0.0-beta.1") bazel_dep(name = "bazel_skylib", version = "1.7.1") +bazel_dep(name = "bazel_features", version = "1.48.1") bazel_dep(name = "rules_shell", version = "0.6.1") +git_override( + module_name = "bazel_features", + commit = "bcc80f8223e1e69343eb25e6d2eee25f23e0fc30", + remote = "git@github.com:bazel-contrib/bazel_features.git", +) + node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node") # Note, this gets the default version of Node.js from From b52161ab803d24ccb45e845fa03f72ffe71bf6bc Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Wed, 17 Jun 2026 14:06:45 +0200 Subject: [PATCH 5/6] Use bazel features 1.49.0 --- e2e/nodejs_host/MODULE.bazel | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/e2e/nodejs_host/MODULE.bazel b/e2e/nodejs_host/MODULE.bazel index 23f039d3d2..dd30590070 100644 --- a/e2e/nodejs_host/MODULE.bazel +++ b/e2e/nodejs_host/MODULE.bazel @@ -6,15 +6,9 @@ local_path_override( bazel_dep(name = "bazel_lib", version = "3.0.0-beta.1") bazel_dep(name = "bazel_skylib", version = "1.7.1") -bazel_dep(name = "bazel_features", version = "1.48.1") +bazel_dep(name = "bazel_features", version = "1.49.0") bazel_dep(name = "rules_shell", version = "0.6.1") -git_override( - module_name = "bazel_features", - commit = "bcc80f8223e1e69343eb25e6d2eee25f23e0fc30", - remote = "git@github.com:bazel-contrib/bazel_features.git", -) - node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node") # Note, this gets the default version of Node.js from From ec96ebcc27772784aece83fcd9798d9c8f33216b Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 18 Jun 2026 10:07:52 +0200 Subject: [PATCH 6/6] Return as much extension metadata as possible --- nodejs/BUILD.bazel | 1 + nodejs/extensions.bzl | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/nodejs/BUILD.bazel b/nodejs/BUILD.bazel index aa8cf9a1c2..202be5a59d 100644 --- a/nodejs/BUILD.bazel +++ b/nodejs/BUILD.bazel @@ -14,6 +14,7 @@ bzl_library( "//nodejs/private:fetch_node_repositories", "//nodejs/private:node_versions", "//nodejs/private:version_from_attr", + "@bazel_features//:features", ], ) diff --git a/nodejs/extensions.bzl b/nodejs/extensions.bzl index 496023933a..c99954ed11 100644 --- a/nodejs/extensions.bzl +++ b/nodejs/extensions.bzl @@ -9,6 +9,7 @@ use_repo(node, "nodejs_toolchains") ``` """ +load("@bazel_features//:features.bzl", "bazel_features") load("//nodejs/private:fetch_node_repositories.bzl", "fetch_node_repositories") load("//nodejs/private:node_versions.bzl", "NODE_VERSIONS") load("//nodejs/private:version_from_attr.bzl", "version_from_attr") @@ -80,14 +81,20 @@ def _toolchain_extension(module_ctx): register = False, ) - if supports_facts: - return module_ctx.extension_metadata( - reproducible = True, - facts = new_repository_facts, - ) - else: + if not hasattr(module_ctx, "extension_metadata"): return # buildifier: disable=return-value (allow no value) + if not bazel_features.external_deps.extension_metadata_has_reproducible: + return module_ctx.extension_metadata() + + if not supports_facts: + return module_ctx.extension_metadata(reproducible = True) + + return module_ctx.extension_metadata( + reproducible = True, + facts = new_repository_facts, + ) + _ATTRS = { "name": attr.string( doc = "Base name for generated repositories",