Skip to content

Commit 5e17a29

Browse files
committed
fix: provide both exec and target platform optional deps
1 parent dc0205c commit 5e17a29

11 files changed

Lines changed: 414 additions & 11 deletions

MODULE.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,9 @@ npm.npm_translate_lock(
200200
# which runs the `build` package.json script: https://github.com/kubernetes-client/javascript/blob/fc681991e61c6808dd26012a2331f83671a11218/package.json#L28.
201201
# Here we run run build so we just run `tsc` instead of `npm run build` which ends up just running `tsc`.
202202
"@kubernetes/client-node": ["build"],
203+
# esbuild's install.js runs maybeOptimizePackage() which hard-links the native Go binary over bin/esbuild,
204+
# replacing the JS launcher that js_binary needs to invoke via node. Disabling the hook preserves the JS launcher.
205+
"esbuild": [],
203206
# 'install' hook fails as it assumes the following path to `node-pre-gyp`: ./node_modules/.bin/node-pre-gyp
204207
# https://github.com/stultuss/protoc-gen-grpc-ts/blob/53d52a9d0e1fe3cbe930dec5581eca89b3dde807/package.json#L28
205208

npm/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ bzl_library(
6262
srcs = ["extensions.bzl"],
6363
visibility = ["//visibility:public"],
6464
deps = [
65+
"//npm/private:npm_exec_platform",
6566
"//npm/private:npm_import",
6667
"//npm/private:npm_translate_lock",
6768
"//npm/private:npm_translate_lock_generate",

npm/extensions.bzl

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use_repo(npm, "npm")
2929
"""
3030

3131
load("@bazel_lib//lib:repo_utils.bzl", "repo_utils")
32+
load("//npm/private:npm_exec_platform.bzl", "npm_exec_platform_detect")
3233
load("//npm/private:npm_import.bzl", "npm_import", "npm_import_lib")
3334
load("//npm/private:npm_translate_lock.bzl", "npm_translate_lock_lib", "parse_and_verify_lock")
3435
load("//npm/private:npm_translate_lock_generate.bzl", "generate_repository_files")
@@ -69,6 +70,12 @@ def _fail_on_non_root_overrides(module, tag_class):
6970
))
7071

7172
def _npm_extension_impl(module_ctx):
73+
# Create the exec platform detection repo used by _links_defs.bzl select() blocks
74+
# to include exec-platform optional deps (e.g. native binaries) when packages are
75+
# used as build tools. See #2121 and #2754.
76+
npm_exec_platform_detect(name = "rules_js_exec_platform")
77+
exec_platform_repo = "rules_js_exec_platform"
78+
7279
# Collect all exclude_package_contents tags and build exclusion dictionary
7380
exclude_package_contents_config = _build_exclude_package_contents_config(module_ctx)
7481

@@ -86,10 +93,10 @@ def _npm_extension_impl(module_ctx):
8693
# Process npm_translate_lock and npm_import tags
8794
for mod in module_ctx.modules:
8895
for attr in mod.tags.npm_translate_lock:
89-
_npm_translate_lock_bzlmod(module_ctx, mod, attr, exclude_package_contents_config, replace_packages)
96+
_npm_translate_lock_bzlmod(module_ctx, mod, attr, exclude_package_contents_config, replace_packages, exec_platform_repo)
9097

9198
for i in mod.tags.npm_import:
92-
_npm_import_bzlmod(i)
99+
_npm_import_bzlmod(i, exec_platform_repo)
93100

94101
return module_ctx.extension_metadata(reproducible = True)
95102

@@ -133,7 +140,7 @@ _hub_repo = repository_rule(
133140
},
134141
)
135142

136-
def _npm_translate_lock_bzlmod(module_ctx, mod, attr, exclude_package_contents_config, replace_packages):
143+
def _npm_translate_lock_bzlmod(module_ctx, mod, attr, exclude_package_contents_config, replace_packages, exec_platform_repo):
137144
state = parse_and_verify_lock(module_ctx, mod, attr)
138145

139146
module_ctx.report_progress("Generating starlark for npm dependencies")
@@ -208,6 +215,7 @@ WARNING: Cannot determine home directory in order to load home `.npmrc` file in
208215
transitive_closure = i.transitive_closure,
209216
url = i.url,
210217
version = i.version,
218+
exec_platform_repo = exec_platform_repo,
211219
)
212220

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

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

@@ -260,6 +268,7 @@ def _npm_import_bzlmod(i):
260268
transitive_closure = None,
261269
url = i.url,
262270
version = i.version,
271+
exec_platform_repo = exec_platform_repo,
263272
)
264273

265274
_NPM_IMPORT_ATTRS = npm_import_lib.attrs | {

npm/private/BUILD.bazel

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ bzl_library(
6262
],
6363
)
6464

65+
bzl_library(
66+
name = "npm_exec_platform",
67+
srcs = ["npm_exec_platform.bzl"],
68+
deps = [
69+
"@bazel_skylib//rules:common_settings",
70+
],
71+
)
72+
6573
bzl_library(
6674
name = "npm_import",
6775
srcs = ["npm_import.bzl"],

npm/private/npm_exec_platform.bzl

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"""Repository rule to detect the exec platform and expose it as build flags.
2+
3+
This generates a repository with string_flag targets defaulting to the exec
4+
platform's OS and CPU, plus config_setting targets for all pnpm platform
5+
combinations. This allows generated _links_defs.bzl files to include
6+
exec-platform select() blocks that are stable across machines.
7+
"""
8+
9+
# OS values from PNPM_PLATFORMS (those with non-None bazel targets)
10+
_PNPM_OS_VALUES = [
11+
"aix",
12+
"android",
13+
"darwin",
14+
"freebsd",
15+
"linux",
16+
"netbsd",
17+
"openbsd",
18+
"win32",
19+
]
20+
21+
# CPU values from PNPM_ARCHS (those with non-None bazel targets)
22+
_PNPM_CPU_VALUES = [
23+
"arm",
24+
"arm64",
25+
"ia32",
26+
"mips",
27+
"ppc",
28+
"ppc64",
29+
"riscv64",
30+
"s390x",
31+
"wasm32",
32+
"x64",
33+
]
34+
35+
def _rctx_os_to_pnpm(os_name):
36+
"""Map rctx.os.name to a pnpm OS string."""
37+
mapping = {
38+
"linux": "linux",
39+
"mac os x": "darwin",
40+
"windows": "win32",
41+
"freebsd": "freebsd",
42+
"openbsd": "openbsd",
43+
}
44+
return mapping.get(os_name.lower(), "linux")
45+
46+
def _rctx_cpu_to_pnpm(arch):
47+
"""Map rctx.os.arch to a pnpm CPU string."""
48+
mapping = {
49+
"amd64": "x64",
50+
"x86_64": "x64",
51+
"aarch64": "arm64",
52+
"arm64": "arm64",
53+
"x86": "ia32",
54+
"i386": "ia32",
55+
"i486": "ia32",
56+
"i586": "ia32",
57+
"i686": "ia32",
58+
}
59+
return mapping.get(arch.lower(), "x64")
60+
61+
def _npm_exec_platform_impl(rctx):
62+
pnpm_os = _rctx_os_to_pnpm(rctx.os.name)
63+
pnpm_cpu = _rctx_cpu_to_pnpm(rctx.os.arch)
64+
65+
os_values_str = "[{}]".format(", ".join(['"{}"'.format(v) for v in _PNPM_OS_VALUES]))
66+
cpu_values_str = "[{}]".format(", ".join(['"{}"'.format(v) for v in _PNPM_CPU_VALUES]))
67+
68+
lines = [
69+
'load("@bazel_skylib//rules:common_settings.bzl", "string_flag")',
70+
"",
71+
"string_flag(",
72+
' name = "os",',
73+
' build_setting_default = "{}",'.format(pnpm_os),
74+
" values = {},".format(os_values_str),
75+
' visibility = ["//visibility:public"],',
76+
")",
77+
"",
78+
"string_flag(",
79+
' name = "cpu",',
80+
' build_setting_default = "{}",'.format(pnpm_cpu),
81+
" values = {},".format(cpu_values_str),
82+
' visibility = ["//visibility:public"],',
83+
")",
84+
"",
85+
]
86+
87+
# OS-only config_settings
88+
for os_val in _PNPM_OS_VALUES:
89+
lines += [
90+
"config_setting(",
91+
' name = "{}",'.format(os_val),
92+
' flag_values = {{":os": "{}"}},'.format(os_val),
93+
' visibility = ["//visibility:public"],',
94+
")",
95+
"",
96+
]
97+
98+
# CPU-only config_settings
99+
for cpu_val in _PNPM_CPU_VALUES:
100+
lines += [
101+
"config_setting(",
102+
' name = "{}",'.format(cpu_val),
103+
' flag_values = {{":cpu": "{}"}},'.format(cpu_val),
104+
' visibility = ["//visibility:public"],',
105+
")",
106+
"",
107+
]
108+
109+
# OS+CPU config_settings
110+
for os_val in _PNPM_OS_VALUES:
111+
for cpu_val in _PNPM_CPU_VALUES:
112+
lines += [
113+
"config_setting(",
114+
' name = "{}_{}",'.format(os_val, cpu_val),
115+
' flag_values = {{":os": "{}", ":cpu": "{}"}},'.format(os_val, cpu_val),
116+
' visibility = ["//visibility:public"],',
117+
")",
118+
"",
119+
]
120+
121+
rctx.file("BUILD.bazel", "\n".join(lines))
122+
123+
# Support bazel <v8.3 by returning None if repo_metadata is not defined
124+
if not hasattr(rctx, "repo_metadata"):
125+
return None
126+
127+
return rctx.repo_metadata(reproducible = False)
128+
129+
npm_exec_platform_detect = repository_rule(
130+
implementation = _npm_exec_platform_impl,
131+
doc = """Detects the exec platform and generates string_flag + config_setting targets.
132+
133+
Used internally by rules_js to allow _links_defs.bzl files to include
134+
exec-platform select() conditions for optional platform-specific npm deps.
135+
""",
136+
)

npm/private/npm_import.bzl

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,17 @@ def _npm_import_links_rule_impl(rctx):
733733
deps_oss = {}
734734
deps_cpus = {}
735735

736+
# When exec_platform_repo is set (bzlmod mode), compute the canonical name of
737+
# the exec-platform repo so _links_defs.bzl can emit @@canonical//:condition
738+
# select() keys. We derive the canonical prefix from rctx.name (the canonical
739+
# name of THIS links repo) by stripping the known apparent name suffix. This
740+
# avoids hardcoding the separator character ('~' in Bazel 7, '+' in Bazel 8).
741+
# See #2121/#2754.
742+
exec_platform_repo = None
743+
if rctx.attr.exec_platform_repo:
744+
prefix = rctx.name[:-len(rctx.attr.apparent_name)]
745+
exec_platform_repo = prefix + rctx.attr.exec_platform_repo
746+
736747
has_lifecycle_build_target = bool(rctx.attr.lifecycle_build_target)
737748
has_transitive_closure = len(rctx.attr.transitive_closure) > 0
738749

@@ -795,7 +806,6 @@ def _npm_import_links_rule_impl(rctx):
795806
dep_cpus = rctx.attr.deps_cpus.get(dep_key, None)
796807
if dep_cpus:
797808
deps_cpus[dep_store_target] = dep_cpus
798-
799809
if has_lifecycle_build_target:
800810
# Lifecycle hooks require a self-reference. Use the regular package name if not named via transitive_closure.
801811
self_ref_names = self_ref_names if self_ref_names else [rctx.attr.package]
@@ -836,7 +846,6 @@ def _npm_import_links_rule_impl(rctx):
836846
lc_deps[dep] = ",".join(lc_deps[dep])
837847
for dep in ref_deps.keys():
838848
ref_deps[dep] = ",".join(ref_deps[dep])
839-
840849
lifecycle_hooks_env = {}
841850
for env in rctx.attr.lifecycle_hooks_env:
842851
key_value = env.split("=", 1)
@@ -855,7 +864,7 @@ def _npm_import_links_rule_impl(rctx):
855864
public_visibility = ("//visibility:public" in rctx.attr.package_visibility)
856865

857866
npm_link_pkg_bzl_vars = dict(
858-
deps = _to_deps_attr(deps, deps_oss, deps_cpus),
867+
deps = _to_deps_attr(deps, deps_oss, deps_cpus, exec_platform_repo),
859868
npm_package_target = npm_package_target,
860869
lc_deps = _to_deps_attr(lc_deps, deps_oss, deps_cpus) if has_lifecycle_build_target else "{}",
861870
has_lifecycle_build_target = has_lifecycle_build_target,
@@ -898,7 +907,16 @@ def _npm_import_links_rule_impl(rctx):
898907

899908
return rctx.repo_metadata(reproducible = True)
900909

901-
def _to_deps_attr(deps, deps_oss, deps_cpus):
910+
def _to_exec_platform_condition(target_condition, exec_platform_repo):
911+
"""Transform a target-platform condition label to an exec-platform condition label.
912+
913+
target_condition is like "@aspect_rules_js//platforms/pnpm:darwin_arm64".
914+
Returns "@@<exec_platform_repo>//:darwin_arm64".
915+
"""
916+
name = target_condition.split(":")[-1]
917+
return "@@{}//:{}".format(exec_platform_repo, name)
918+
919+
def _to_deps_attr(deps, deps_oss, deps_cpus, exec_platform_repo = None):
902920
# Must split the deps into groups that share the same constraints based on
903921
# cpu and os conditions.
904922
# A bazel select() can only have one truthy condition so constraints such as:
@@ -911,28 +929,56 @@ def _to_deps_attr(deps, deps_oss, deps_cpus):
911929
"both": {},
912930
}
913931

932+
# Generate parallel exec-platform select() blocks so that optional
933+
# platform-specific deps are present when the package is used as a build
934+
# tool running on the exec machine. See #2121 and #2754.
935+
exec_constrained = {
936+
"os": {},
937+
"cpu": {},
938+
"both": {},
939+
}
940+
914941
for k, v in deps.items():
915942
if k in deps_oss and k in deps_cpus:
916943
for condition in pnpm.to_bazel_os_cpu_constraints(deps_oss[k], deps_cpus[k]):
917944
if condition not in constrained["both"]:
918945
constrained["both"][condition] = {}
919946
constrained["both"][condition][k] = v
947+
if exec_platform_repo != None:
948+
ec = _to_exec_platform_condition(condition, exec_platform_repo)
949+
if ec not in exec_constrained["both"]:
950+
exec_constrained["both"][ec] = {}
951+
exec_constrained["both"][ec][k] = v
920952
elif k in deps_oss:
921953
for condition in pnpm.to_bazel_os_constraints(deps_oss[k]):
922954
if condition not in constrained["os"]:
923955
constrained["os"][condition] = {}
924956
constrained["os"][condition][k] = v
957+
if exec_platform_repo != None:
958+
ec = _to_exec_platform_condition(condition, exec_platform_repo)
959+
if ec not in exec_constrained["os"]:
960+
exec_constrained["os"][ec] = {}
961+
exec_constrained["os"][ec][k] = v
925962
elif k in deps_cpus:
926963
for condition in pnpm.to_bazel_cpu_constraints(deps_cpus[k]):
927964
if condition not in constrained["cpu"]:
928965
constrained["cpu"][condition] = {}
929966
constrained["cpu"][condition][k] = v
967+
if exec_platform_repo != None:
968+
ec = _to_exec_platform_condition(condition, exec_platform_repo)
969+
if ec not in exec_constrained["cpu"]:
970+
exec_constrained["cpu"][ec] = {}
971+
exec_constrained["cpu"][ec][k] = v
930972
else:
931973
unconstrained[k] = v
932974

975+
groups = [v for v in constrained.values() if len(v) > 0]
976+
if exec_platform_repo != None:
977+
groups = groups + [v for v in exec_constrained.values() if len(v) > 0]
978+
933979
return starlark_codegen_utils.to_conditional_dict_attr(
934980
unconstrained,
935-
[v for v in constrained.values() if len(v) > 0],
981+
groups,
936982
quote_key = False,
937983
indent_count = 2,
938984
)
@@ -1207,6 +1253,8 @@ npm_import_links_rule = repository_rule(
12071253
"deps_oss": attr.string_list_dict(),
12081254
"deps_cpus": attr.string_list_dict(),
12091255
"transitive_closure": attr.string_list_dict(doc = "Mapping of package store entry labels to a list of names to reference that package as"),
1256+
"apparent_name": attr.string(doc = "The apparent (non-canonical) name of this repository rule; used to derive the canonical name prefix for sibling repos."),
1257+
"exec_platform_repo": attr.string(doc = "Apparent name of the exec-platform detection repo; used for select() conditions in _links_defs.bzl."),
12101258
},
12111259
)
12121260

@@ -1256,7 +1304,8 @@ def npm_import(
12561304
generate_package_json_bzl,
12571305
extract_full_archive,
12581306
exclude_package_contents,
1259-
exclude_package_contents_presets):
1307+
exclude_package_contents_presets,
1308+
exec_platform_repo = None):
12601309
# By convention, the `{name}` repository contains the actual npm
12611310
# package sources downloaded from the registry and extracted
12621311
npm_import_rule(
@@ -1291,8 +1340,10 @@ def npm_import(
12911340

12921341
# By convention, the `{name}{utils.links_repo_suffix}` repository contains the generated
12931342
# code to link this npm package into one or more node_modules trees
1343+
links_name = "{}{}".format(name, utils.links_repo_suffix)
12941344
npm_import_links_rule(
1295-
name = "{}{}".format(name, utils.links_repo_suffix),
1345+
name = links_name,
1346+
apparent_name = links_name,
12961347
key = key,
12971348
package = package,
12981349
version = version,
@@ -1310,4 +1361,5 @@ def npm_import(
13101361
replace_package = replace_package,
13111362
exclude_package_contents = exclude_package_contents,
13121363
exclude_package_contents_presets = exclude_package_contents_presets,
1364+
exec_platform_repo = exec_platform_repo,
13131365
)

0 commit comments

Comments
 (0)