Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions npm/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ bzl_library(
srcs = ["extensions.bzl"],
visibility = ["//visibility:public"],
deps = [
"//npm/private:npm_exec_platform",
"//npm/private:npm_import",
"//npm/private:npm_translate_lock",
"//npm/private:npm_translate_lock_generate",
Expand Down
17 changes: 13 additions & 4 deletions npm/extensions.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use_repo(npm, "npm")
"""

load("@bazel_lib//lib:repo_utils.bzl", "repo_utils")
load("//npm/private:npm_exec_platform.bzl", "npm_exec_platform_detect")
load("//npm/private:npm_import.bzl", "npm_import", "npm_import_lib")
load("//npm/private:npm_translate_lock.bzl", "npm_translate_lock_lib", "parse_and_verify_lock")
load("//npm/private:npm_translate_lock_generate.bzl", "generate_repository_files")
Expand Down Expand Up @@ -69,6 +70,12 @@ def _fail_on_non_root_overrides(module, tag_class):
))

def _npm_extension_impl(module_ctx):
# Create the exec platform detection repo used by _links_defs.bzl select() blocks
# to include exec-platform optional deps (e.g. native binaries) when packages are
# used as build tools. See #2121 and #2754.
npm_exec_platform_detect(name = "rules_js_exec_platform")
exec_platform_repo = "rules_js_exec_platform"

# Collect all exclude_package_contents tags and build exclusion dictionary
exclude_package_contents_config = _build_exclude_package_contents_config(module_ctx)

Expand All @@ -86,10 +93,10 @@ def _npm_extension_impl(module_ctx):
# Process npm_translate_lock and npm_import tags
for mod in module_ctx.modules:
for attr in mod.tags.npm_translate_lock:
_npm_translate_lock_bzlmod(module_ctx, mod, attr, exclude_package_contents_config, replace_packages)
_npm_translate_lock_bzlmod(module_ctx, mod, attr, exclude_package_contents_config, replace_packages, exec_platform_repo)

for i in mod.tags.npm_import:
_npm_import_bzlmod(i)
_npm_import_bzlmod(i, exec_platform_repo)

return module_ctx.extension_metadata(reproducible = True)

Expand Down Expand Up @@ -133,7 +140,7 @@ _hub_repo = repository_rule(
},
)

def _npm_translate_lock_bzlmod(module_ctx, mod, attr, exclude_package_contents_config, replace_packages):
def _npm_translate_lock_bzlmod(module_ctx, mod, attr, exclude_package_contents_config, replace_packages, exec_platform_repo):
state = parse_and_verify_lock(module_ctx, mod, attr)

module_ctx.report_progress("Generating starlark for npm dependencies")
Expand Down Expand Up @@ -208,6 +215,7 @@ WARNING: Cannot determine home directory in order to load home `.npmrc` file in
transitive_closure = i.transitive_closure,
url = i.url,
version = i.version,
exec_platform_repo = exec_platform_repo,
)

files = generate_repository_files(
Expand All @@ -221,7 +229,7 @@ WARNING: Cannot determine home directory in order to load home `.npmrc` file in
contents = files,
)

def _npm_import_bzlmod(i):
def _npm_import_bzlmod(i, exec_platform_repo):
# Assume package+version is a unique key for any package store this import is placed in
package_key = "{}@{}".format(i.package, i.version)

Expand Down Expand Up @@ -260,6 +268,7 @@ def _npm_import_bzlmod(i):
transitive_closure = None,
url = i.url,
version = i.version,
exec_platform_repo = exec_platform_repo,
)

_NPM_IMPORT_ATTRS = npm_import_lib.attrs | {
Expand Down
8 changes: 8 additions & 0 deletions npm/private/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ bzl_library(
],
)

bzl_library(
name = "npm_exec_platform",
srcs = ["npm_exec_platform.bzl"],
deps = [
"@bazel_skylib//rules:common_settings",
],
)

bzl_library(
name = "npm_import",
srcs = ["npm_import.bzl"],
Expand Down
136 changes: 136 additions & 0 deletions npm/private/npm_exec_platform.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Repository rule to detect the exec platform and expose it as build flags.

This generates a repository with string_flag targets defaulting to the exec
platform's OS and CPU, plus config_setting targets for all pnpm platform
combinations. This allows generated _links_defs.bzl files to include
exec-platform select() blocks that are stable across machines.
"""

# OS values from PNPM_PLATFORMS (those with non-None bazel targets)
_PNPM_OS_VALUES = [
"aix",
"android",
"darwin",
"freebsd",
"linux",
"netbsd",
"openbsd",
"win32",
]

# CPU values from PNPM_ARCHS (those with non-None bazel targets)
_PNPM_CPU_VALUES = [
"arm",
"arm64",
"ia32",
"mips",
"ppc",
"ppc64",
"riscv64",
"s390x",
"wasm32",
"x64",
]

def _rctx_os_to_pnpm(os_name):
"""Map rctx.os.name to a pnpm OS string."""
mapping = {
"linux": "linux",
"mac os x": "darwin",
"windows": "win32",
"freebsd": "freebsd",
"openbsd": "openbsd",
}
return mapping.get(os_name.lower(), "linux")

def _rctx_cpu_to_pnpm(arch):
"""Map rctx.os.arch to a pnpm CPU string."""
mapping = {
"amd64": "x64",
"x86_64": "x64",
"aarch64": "arm64",
"arm64": "arm64",
"x86": "ia32",
"i386": "ia32",
"i486": "ia32",
"i586": "ia32",
"i686": "ia32",
}
return mapping.get(arch.lower(), "x64")

def _npm_exec_platform_impl(rctx):
pnpm_os = _rctx_os_to_pnpm(rctx.os.name)
pnpm_cpu = _rctx_cpu_to_pnpm(rctx.os.arch)

os_values_str = "[{}]".format(", ".join(['"{}"'.format(v) for v in _PNPM_OS_VALUES]))
cpu_values_str = "[{}]".format(", ".join(['"{}"'.format(v) for v in _PNPM_CPU_VALUES]))

lines = [
'load("@bazel_skylib//rules:common_settings.bzl", "string_flag")',
"",
"string_flag(",
' name = "os",',
' build_setting_default = "{}",'.format(pnpm_os),
" values = {},".format(os_values_str),
' visibility = ["//visibility:public"],',
")",
"",
"string_flag(",
' name = "cpu",',
' build_setting_default = "{}",'.format(pnpm_cpu),
" values = {},".format(cpu_values_str),
' visibility = ["//visibility:public"],',
")",
"",
]

# OS-only config_settings
for os_val in _PNPM_OS_VALUES:
lines += [
"config_setting(",
' name = "{}",'.format(os_val),
' flag_values = {{":os": "{}"}},'.format(os_val),
' visibility = ["//visibility:public"],',
")",
"",
]

# CPU-only config_settings
for cpu_val in _PNPM_CPU_VALUES:
lines += [
"config_setting(",
' name = "{}",'.format(cpu_val),
' flag_values = {{":cpu": "{}"}},'.format(cpu_val),
' visibility = ["//visibility:public"],',
")",
"",
]

# OS+CPU config_settings
for os_val in _PNPM_OS_VALUES:
for cpu_val in _PNPM_CPU_VALUES:
lines += [
"config_setting(",
' name = "{}_{}",'.format(os_val, cpu_val),
' flag_values = {{":os": "{}", ":cpu": "{}"}},'.format(os_val, cpu_val),
' visibility = ["//visibility:public"],',
")",
"",
]

rctx.file("BUILD.bazel", "\n".join(lines))

# Support bazel <v8.3 by returning None if repo_metadata is not defined
if not hasattr(rctx, "repo_metadata"):
return None

return rctx.repo_metadata(reproducible = False)

npm_exec_platform_detect = repository_rule(
implementation = _npm_exec_platform_impl,
doc = """Detects the exec platform and generates string_flag + config_setting targets.

Used internally by rules_js to allow _links_defs.bzl files to include
exec-platform select() conditions for optional platform-specific npm deps.
""",
)
66 changes: 59 additions & 7 deletions npm/private/npm_import.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,17 @@ def _npm_import_links_rule_impl(rctx):
deps_oss = {}
deps_cpus = {}

# When exec_platform_repo is set (bzlmod mode), compute the canonical name of
# the exec-platform repo so _links_defs.bzl can emit @@canonical//:condition
# select() keys. We derive the canonical prefix from rctx.name (the canonical
# name of THIS links repo) by stripping the known apparent name suffix. This
# avoids hardcoding the separator character ('~' in Bazel 7, '+' in Bazel 8).
# See #2121/#2754.
exec_platform_repo = None
if rctx.attr.exec_platform_repo:
prefix = rctx.name[:-len(rctx.attr.apparent_name)]
exec_platform_repo = prefix + rctx.attr.exec_platform_repo

has_lifecycle_build_target = bool(rctx.attr.lifecycle_build_target)
has_transitive_closure = len(rctx.attr.transitive_closure) > 0

Expand Down Expand Up @@ -795,7 +806,6 @@ def _npm_import_links_rule_impl(rctx):
dep_cpus = rctx.attr.deps_cpus.get(dep_key, None)
if dep_cpus:
deps_cpus[dep_store_target] = dep_cpus

if has_lifecycle_build_target:
# Lifecycle hooks require a self-reference. Use the regular package name if not named via transitive_closure.
self_ref_names = self_ref_names if self_ref_names else [rctx.attr.package]
Expand Down Expand Up @@ -836,7 +846,6 @@ def _npm_import_links_rule_impl(rctx):
lc_deps[dep] = ",".join(lc_deps[dep])
for dep in ref_deps.keys():
ref_deps[dep] = ",".join(ref_deps[dep])

lifecycle_hooks_env = {}
for env in rctx.attr.lifecycle_hooks_env:
key_value = env.split("=", 1)
Expand All @@ -855,7 +864,7 @@ def _npm_import_links_rule_impl(rctx):
public_visibility = ("//visibility:public" in rctx.attr.package_visibility)

npm_link_pkg_bzl_vars = dict(
deps = _to_deps_attr(deps, deps_oss, deps_cpus),
deps = _to_deps_attr(deps, deps_oss, deps_cpus, exec_platform_repo),
npm_package_target = npm_package_target,
lc_deps = _to_deps_attr(lc_deps, deps_oss, deps_cpus) if has_lifecycle_build_target else "{}",
has_lifecycle_build_target = has_lifecycle_build_target,
Expand Down Expand Up @@ -898,7 +907,16 @@ def _npm_import_links_rule_impl(rctx):

return rctx.repo_metadata(reproducible = True)

def _to_deps_attr(deps, deps_oss, deps_cpus):
def _to_exec_platform_condition(target_condition, exec_platform_repo):
"""Transform a target-platform condition label to an exec-platform condition label.

target_condition is like "@aspect_rules_js//platforms/pnpm:darwin_arm64".
Returns "@@<exec_platform_repo>//:darwin_arm64".
"""
name = target_condition.split(":")[-1]
return "@@{}//:{}".format(exec_platform_repo, name)

def _to_deps_attr(deps, deps_oss, deps_cpus, exec_platform_repo = None):
# Must split the deps into groups that share the same constraints based on
# cpu and os conditions.
# A bazel select() can only have one truthy condition so constraints such as:
Expand All @@ -911,28 +929,56 @@ def _to_deps_attr(deps, deps_oss, deps_cpus):
"both": {},
}

# Generate parallel exec-platform select() blocks so that optional
# platform-specific deps are present when the package is used as a build
# tool running on the exec machine. See #2121 and #2754.
exec_constrained = {
"os": {},
"cpu": {},
"both": {},
}

for k, v in deps.items():
if k in deps_oss and k in deps_cpus:
for condition in pnpm.to_bazel_os_cpu_constraints(deps_oss[k], deps_cpus[k]):
if condition not in constrained["both"]:
constrained["both"][condition] = {}
constrained["both"][condition][k] = v
if exec_platform_repo != None:
ec = _to_exec_platform_condition(condition, exec_platform_repo)
if ec not in exec_constrained["both"]:
exec_constrained["both"][ec] = {}
exec_constrained["both"][ec][k] = v
elif k in deps_oss:
for condition in pnpm.to_bazel_os_constraints(deps_oss[k]):
if condition not in constrained["os"]:
constrained["os"][condition] = {}
constrained["os"][condition][k] = v
if exec_platform_repo != None:
ec = _to_exec_platform_condition(condition, exec_platform_repo)
if ec not in exec_constrained["os"]:
exec_constrained["os"][ec] = {}
exec_constrained["os"][ec][k] = v
elif k in deps_cpus:
for condition in pnpm.to_bazel_cpu_constraints(deps_cpus[k]):
if condition not in constrained["cpu"]:
constrained["cpu"][condition] = {}
constrained["cpu"][condition][k] = v
if exec_platform_repo != None:
ec = _to_exec_platform_condition(condition, exec_platform_repo)
if ec not in exec_constrained["cpu"]:
exec_constrained["cpu"][ec] = {}
exec_constrained["cpu"][ec][k] = v
else:
unconstrained[k] = v

groups = [v for v in constrained.values() if len(v) > 0]
if exec_platform_repo != None:
groups = groups + [v for v in exec_constrained.values() if len(v) > 0]

return starlark_codegen_utils.to_conditional_dict_attr(
unconstrained,
[v for v in constrained.values() if len(v) > 0],
groups,
quote_key = False,
indent_count = 2,
)
Expand Down Expand Up @@ -1207,6 +1253,8 @@ npm_import_links_rule = repository_rule(
"deps_oss": attr.string_list_dict(),
"deps_cpus": attr.string_list_dict(),
"transitive_closure": attr.string_list_dict(doc = "Mapping of package store entry labels to a list of names to reference that package as"),
"apparent_name": attr.string(doc = "The apparent (non-canonical) name of this repository rule; used to derive the canonical name prefix for sibling repos."),
"exec_platform_repo": attr.string(doc = "Apparent name of the exec-platform detection repo; used for select() conditions in _links_defs.bzl."),
},
)

Expand Down Expand Up @@ -1256,7 +1304,8 @@ def npm_import(
generate_package_json_bzl,
extract_full_archive,
exclude_package_contents,
exclude_package_contents_presets):
exclude_package_contents_presets,
exec_platform_repo = None):
# By convention, the `{name}` repository contains the actual npm
# package sources downloaded from the registry and extracted
npm_import_rule(
Expand Down Expand Up @@ -1291,8 +1340,10 @@ def npm_import(

# By convention, the `{name}{utils.links_repo_suffix}` repository contains the generated
# code to link this npm package into one or more node_modules trees
links_name = "{}{}".format(name, utils.links_repo_suffix)
npm_import_links_rule(
name = "{}{}".format(name, utils.links_repo_suffix),
name = links_name,
apparent_name = links_name,
key = key,
package = package,
version = version,
Expand All @@ -1310,4 +1361,5 @@ def npm_import(
replace_package = replace_package,
exclude_package_contents = exclude_package_contents,
exclude_package_contents_presets = exclude_package_contents_presets,
exec_platform_repo = exec_platform_repo,
)
Loading
Loading