From 784dcfd96329d2e1968e6f215516865d90bbbb7e Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Wed, 8 Apr 2026 11:14:21 -0700 Subject: [PATCH] fix: provide both exec and target platform optional deps --- npm/BUILD.bazel | 1 + npm/extensions.bzl | 17 ++- npm/private/BUILD.bazel | 8 ++ npm/private/npm_exec_platform.bzl | 136 ++++++++++++++++++ npm/private/npm_import.bzl | 66 ++++++++- .../snapshots/closure-compiler_links_defs.bzl | 20 +++ .../test/snapshots/rollup_links_defs.bzl | 61 ++++++++ 7 files changed, 298 insertions(+), 11 deletions(-) create mode 100644 npm/private/npm_exec_platform.bzl diff --git a/npm/BUILD.bazel b/npm/BUILD.bazel index e618f9fa51..b13368eba4 100644 --- a/npm/BUILD.bazel +++ b/npm/BUILD.bazel @@ -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", diff --git a/npm/extensions.bzl b/npm/extensions.bzl index 5cd11e3b31..5848eabc62 100644 --- a/npm/extensions.bzl +++ b/npm/extensions.bzl @@ -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") @@ -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) @@ -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) @@ -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") @@ -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( @@ -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) @@ -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 | { diff --git a/npm/private/BUILD.bazel b/npm/private/BUILD.bazel index 999b141f7b..ad0c353fe5 100644 --- a/npm/private/BUILD.bazel +++ b/npm/private/BUILD.bazel @@ -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"], diff --git a/npm/private/npm_exec_platform.bzl b/npm/private/npm_exec_platform.bzl new file mode 100644 index 0000000000..29083e4299 --- /dev/null +++ b/npm/private/npm_exec_platform.bzl @@ -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 0 @@ -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] @@ -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) @@ -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, @@ -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 "@@//: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: @@ -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, ) @@ -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."), }, ) @@ -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( @@ -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, @@ -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, ) diff --git a/npm/private/test/snapshots/closure-compiler_links_defs.bzl b/npm/private/test/snapshots/closure-compiler_links_defs.bzl index d3177154f6..057123c7a3 100644 --- a/npm/private/test/snapshots/closure-compiler_links_defs.bzl +++ b/npm/private/test/snapshots/closure-compiler_links_defs.bzl @@ -48,6 +48,26 @@ def npm_imported_package_store_internal(): ":.aspect_rules_js/node_modules/google-closure-compiler-windows@20251111.0.0": "google-closure-compiler-windows", }, "//conditions:default": {}, + }) | select({ + "@@_main~npm~rules_js_exec_platform//:linux_ia32": { + ":.aspect_rules_js/node_modules/google-closure-compiler-linux@20251111.0.0": "google-closure-compiler-linux", + }, + "@@_main~npm~rules_js_exec_platform//:linux_x64": { + ":.aspect_rules_js/node_modules/google-closure-compiler-linux@20251111.0.0": "google-closure-compiler-linux", + }, + "@@_main~npm~rules_js_exec_platform//:linux_arm64": { + ":.aspect_rules_js/node_modules/google-closure-compiler-linux-arm64@20251111.0.0": "google-closure-compiler-linux-arm64", + }, + "@@_main~npm~rules_js_exec_platform//:darwin_arm64": { + ":.aspect_rules_js/node_modules/google-closure-compiler-macos@20251111.0.0": "google-closure-compiler-macos", + }, + "@@_main~npm~rules_js_exec_platform//:win32_ia32": { + ":.aspect_rules_js/node_modules/google-closure-compiler-windows@20251111.0.0": "google-closure-compiler-windows", + }, + "@@_main~npm~rules_js_exec_platform//:win32_x64": { + ":.aspect_rules_js/node_modules/google-closure-compiler-windows@20251111.0.0": "google-closure-compiler-windows", + }, + "//conditions:default": {}, }), ref_deps = { ":.aspect_rules_js/node_modules/chalk@5.1.1/ref": "chalk", diff --git a/npm/private/test/snapshots/rollup_links_defs.bzl b/npm/private/test/snapshots/rollup_links_defs.bzl index 323998a43b..95bd431600 100644 --- a/npm/private/test/snapshots/rollup_links_defs.bzl +++ b/npm/private/test/snapshots/rollup_links_defs.bzl @@ -85,6 +85,67 @@ def npm_imported_package_store_internal(): ":.aspect_rules_js/node_modules/@rollup+rollup-win32-x64-msvc@4.55.2": "@rollup/rollup-win32-x64-msvc", }, "//conditions:default": {}, + }) | select({ + "@@_main~npm~rules_js_exec_platform//:darwin": { + ":.aspect_rules_js/node_modules/fsevents@2.3.3": "fsevents", + }, + "//conditions:default": {}, + }) | select({ + "@@_main~npm~rules_js_exec_platform//:android_arm": { + ":.aspect_rules_js/node_modules/@rollup+rollup-android-arm-eabi@4.55.2": "@rollup/rollup-android-arm-eabi", + }, + "@@_main~npm~rules_js_exec_platform//:android_arm64": { + ":.aspect_rules_js/node_modules/@rollup+rollup-android-arm64@4.55.2": "@rollup/rollup-android-arm64", + }, + "@@_main~npm~rules_js_exec_platform//:darwin_arm64": { + ":.aspect_rules_js/node_modules/@rollup+rollup-darwin-arm64@4.55.2": "@rollup/rollup-darwin-arm64", + }, + "@@_main~npm~rules_js_exec_platform//:darwin_x64": { + ":.aspect_rules_js/node_modules/@rollup+rollup-darwin-x64@4.55.2": "@rollup/rollup-darwin-x64", + }, + "@@_main~npm~rules_js_exec_platform//:freebsd_arm64": { + ":.aspect_rules_js/node_modules/@rollup+rollup-freebsd-arm64@4.55.2": "@rollup/rollup-freebsd-arm64", + }, + "@@_main~npm~rules_js_exec_platform//:freebsd_x64": { + ":.aspect_rules_js/node_modules/@rollup+rollup-freebsd-x64@4.55.2": "@rollup/rollup-freebsd-x64", + }, + "@@_main~npm~rules_js_exec_platform//:linux_arm": { + ":.aspect_rules_js/node_modules/@rollup+rollup-linux-arm-gnueabihf@4.55.2": "@rollup/rollup-linux-arm-gnueabihf", + ":.aspect_rules_js/node_modules/@rollup+rollup-linux-arm-musleabihf@4.55.2": "@rollup/rollup-linux-arm-musleabihf", + }, + "@@_main~npm~rules_js_exec_platform//:linux_arm64": { + ":.aspect_rules_js/node_modules/@rollup+rollup-linux-arm64-gnu@4.55.2": "@rollup/rollup-linux-arm64-gnu", + ":.aspect_rules_js/node_modules/@rollup+rollup-linux-arm64-musl@4.55.2": "@rollup/rollup-linux-arm64-musl", + }, + "@@_main~npm~rules_js_exec_platform//:linux_ppc64": { + ":.aspect_rules_js/node_modules/@rollup+rollup-linux-ppc64-gnu@4.55.2": "@rollup/rollup-linux-ppc64-gnu", + ":.aspect_rules_js/node_modules/@rollup+rollup-linux-ppc64-musl@4.55.2": "@rollup/rollup-linux-ppc64-musl", + }, + "@@_main~npm~rules_js_exec_platform//:linux_riscv64": { + ":.aspect_rules_js/node_modules/@rollup+rollup-linux-riscv64-gnu@4.55.2": "@rollup/rollup-linux-riscv64-gnu", + ":.aspect_rules_js/node_modules/@rollup+rollup-linux-riscv64-musl@4.55.2": "@rollup/rollup-linux-riscv64-musl", + }, + "@@_main~npm~rules_js_exec_platform//:linux_s390x": { + ":.aspect_rules_js/node_modules/@rollup+rollup-linux-s390x-gnu@4.55.2": "@rollup/rollup-linux-s390x-gnu", + }, + "@@_main~npm~rules_js_exec_platform//:linux_x64": { + ":.aspect_rules_js/node_modules/@rollup+rollup-linux-x64-gnu@4.55.2": "@rollup/rollup-linux-x64-gnu", + ":.aspect_rules_js/node_modules/@rollup+rollup-linux-x64-musl@4.55.2": "@rollup/rollup-linux-x64-musl", + }, + "@@_main~npm~rules_js_exec_platform//:openbsd_x64": { + ":.aspect_rules_js/node_modules/@rollup+rollup-openbsd-x64@4.55.2": "@rollup/rollup-openbsd-x64", + }, + "@@_main~npm~rules_js_exec_platform//:win32_arm64": { + ":.aspect_rules_js/node_modules/@rollup+rollup-win32-arm64-msvc@4.55.2": "@rollup/rollup-win32-arm64-msvc", + }, + "@@_main~npm~rules_js_exec_platform//:win32_ia32": { + ":.aspect_rules_js/node_modules/@rollup+rollup-win32-ia32-msvc@4.55.2": "@rollup/rollup-win32-ia32-msvc", + }, + "@@_main~npm~rules_js_exec_platform//:win32_x64": { + ":.aspect_rules_js/node_modules/@rollup+rollup-win32-x64-gnu@4.55.2": "@rollup/rollup-win32-x64-gnu", + ":.aspect_rules_js/node_modules/@rollup+rollup-win32-x64-msvc@4.55.2": "@rollup/rollup-win32-x64-msvc", + }, + "//conditions:default": {}, }), ref_deps = { ":.aspect_rules_js/node_modules/@types+estree@1.0.8/ref": "@types/estree",