diff --git a/e2e/pnpm_repo_install/BUILD.bazel b/e2e/pnpm_repo_install/BUILD.bazel index 12de95b47..3e4ad7d02 100644 --- a/e2e/pnpm_repo_install/BUILD.bazel +++ b/e2e/pnpm_repo_install/BUILD.bazel @@ -20,3 +20,12 @@ sh_test( args = ["$(location @pnpm_v11//:pnpm)"], data = ["@pnpm_v11//:pnpm"], ) + +sh_test( + name = "pnpm_patch_test", + srcs = ["pnpm_patch_test.sh"], + data = [ + "@bazel_tools//tools/bash/runfiles", + "@pnpm_patched//:pnpm", + ], +) diff --git a/e2e/pnpm_repo_install/MODULE.bazel b/e2e/pnpm_repo_install/MODULE.bazel index 5a18b9b4a..df8c8b113 100644 --- a/e2e/pnpm_repo_install/MODULE.bazel +++ b/e2e/pnpm_repo_install/MODULE.bazel @@ -53,4 +53,10 @@ pnpm.pnpm( pnpm_version = "11.0.4", pnpm_version_integrity = "sha512-CjlxZQB6AU7VKRmmHl9GxIubyohATDA+yuzGP2Le9WOJjTxril1epYEes5jP4DqwXaGlzpY/Em1erUwC+TuDww==", ) -use_repo(pnpm, "pnpm", "pnpm_v10", "pnpm_v11") +pnpm.pnpm( + name = "pnpm_patched", + patches = ["//:patches/pnpm_hello.patch"], + pnpm_version = "9.15.9", + pnpm_version_integrity = "sha512-aARhQYk8ZvrQHAeSMRKOmvuJ74fiaR1p5NQO7iKJiClf1GghgbrlW1hBjDolO95lpQXsfF+UA+zlzDzTfc8lMQ==", +) +use_repo(pnpm, "pnpm", "pnpm_patched", "pnpm_v10", "pnpm_v11") diff --git a/e2e/pnpm_repo_install/patches/pnpm_hello.patch b/e2e/pnpm_repo_install/patches/pnpm_hello.patch new file mode 100644 index 000000000..28d84649c --- /dev/null +++ b/e2e/pnpm_repo_install/patches/pnpm_hello.patch @@ -0,0 +1,6 @@ +diff --git a/hello.txt b/hello.txt +new file mode 100644 +--- /dev/null ++++ b/hello.txt +@@ -0,0 +1 @@ ++hello diff --git a/e2e/pnpm_repo_install/pnpm_patch_test.sh b/e2e/pnpm_repo_install/pnpm_patch_test.sh new file mode 100755 index 000000000..b49b545b5 --- /dev/null +++ b/e2e/pnpm_repo_install/pnpm_patch_test.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -o errexit -o nounset -o pipefail + +# --- begin runfiles.bash initialization v3 --- +# Copy-pasted from the Bazel Bash runfiles library v3. +set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash +# shellcheck disable=SC1090 +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \ + source "$0.runfiles/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e +# --- end runfiles.bash initialization v3 --- + +HELLO=$(rlocation "pnpm_patched/package/hello.txt") + +if [ ! -f "${HELLO}" ]; then + echo "ERROR: expected hello.txt to exist at ${HELLO}" + exit 1 +fi + +CONTENT=$(cat "${HELLO}") +if [ "${CONTENT}" != "hello" ]; then + echo "ERROR: expected content 'hello', got '${CONTENT}'" + exit 1 +fi + +echo "PASS: pnpm patch applied successfully" diff --git a/npm/extensions.bzl b/npm/extensions.bzl index 5cd11e3b3..7dcd4cf25 100644 --- a/npm/extensions.bzl +++ b/npm/extensions.bzl @@ -368,6 +368,8 @@ def _pnpm_extension_impl(module_ctx): pnpm_version = pnpm["version"], integrity = pnpm["integrity"], include_npm = pnpm["include_npm"], + patches = pnpm.get("patches", []), + patch_args = pnpm.get("patch_args", ["-p1"]), ) kwargs = {} @@ -402,6 +404,16 @@ pnpm = module_extension( default = None, ), "pnpm_version_integrity": attr.string(), + "patches": attr.label_list( + doc = "Patch files to apply onto the downloaded pnpm package. " + + "Paths in the patches are relative to the npm tarball root " + + "(which begins with `package/`).", + default = [], + ), + "patch_args": attr.string_list( + doc = "Arguments for the patch tool. Defaults to [\"-p1\"].", + default = ["-p1"], + ), }, ), }, diff --git a/npm/private/pnpm_extension.bzl b/npm/private/pnpm_extension.bzl index 84b141f75..cdf5c1dec 100644 --- a/npm/private/pnpm_extension.bzl +++ b/npm/private/pnpm_extension.bzl @@ -28,6 +28,12 @@ def resolve_pnpm_repositories(mctx): # Collect all the module tags and associated versions/integrities/options integrity = {} registrations = {} + + # Patches and patch_args are per-repo-name (NOT per-version) and only + # accepted from the root module. Multiple tags for the same repo name + # concatenate their patches and the last patch_args wins. + patches_by_repo = {} + patch_args_by_repo = {} for mod in mctx.modules: for attr in mod.tags.pnpm: if attr.name != DEFAULT_PNPM_REPO_NAME and not mod.is_root: @@ -37,6 +43,10 @@ def resolve_pnpm_repositories(mctx): """) if not registrations.get(attr.name, False): registrations[attr.name] = {} + if mod.is_root and (getattr(attr, "patches", None) or getattr(attr, "patch_args", None)): + patches_by_repo.setdefault(attr.name, []) + patches_by_repo[attr.name].extend(getattr(attr, "patches", []) or []) + patch_args_by_repo[attr.name] = list(attr.patch_args) if attr.pnpm_version_from and attr.pnpm_version and attr.pnpm_version != DEFAULT_PNPM_VERSION: fail("Cannot specify both pnpm_version = {} and pnpm_version_from = {}".format(attr.pnpm_version, attr.pnpm_version_from)) @@ -108,6 +118,8 @@ def resolve_pnpm_repositories(mctx): "version": selected, "include_npm": 0 < len([i for i in versions_map[selected] if i]), "integrity": integrity.get(selected, None), + "patches": patches_by_repo.get(name, []), + "patch_args": patch_args_by_repo.get(name, ["-p1"]), } repositories[name] = selected diff --git a/npm/private/pnpm_repository.bzl b/npm/private/pnpm_repository.bzl index d3ee61840..c449969f0 100644 --- a/npm/private/pnpm_repository.bzl +++ b/npm/private/pnpm_repository.bzl @@ -10,7 +10,7 @@ LATEST_PNPM_VERSION = PNPM_VERSIONS.keys()[-1] # Default to the latest pnpm v10 DEFAULT_PNPM_VERSION = [v for v in PNPM_VERSIONS.keys() if v.startswith("10")][-1] -def pnpm_repository(name, pnpm_version, include_npm, integrity): +def pnpm_repository(name, pnpm_version, include_npm, integrity, patches = [], patch_args = ["-p1"]): """Import https://npmjs.com/package/pnpm and provide a js_binary to run the tool. Useful as a way to run exactly the same pnpm as Bazel does, for example with: @@ -24,6 +24,10 @@ def pnpm_repository(name, pnpm_version, include_npm, integrity): `curl --silent https://registry.npmjs.org/pnpm | jq '.versions["8.6.11"].dist.integrity'` integrity: integrity hash for the pnpm version (optional) include_npm: if True, include the npm package along with pnpm binary + patches: list of Label targets pointing to .patch files to apply to the + extracted pnpm package (paths relative to the tarball root, which + starts with "package/"). Forwarded to the underlying npm_import. + patch_args: list of arguments for the patch tool. Defaults to ["-p1"]. """ if native.existing_rule(name): @@ -42,6 +46,8 @@ def pnpm_repository(name, pnpm_version, include_npm, integrity): package = "pnpm", root_package = "", version = pnpm_version, + patches = patches, + patch_args = patch_args, extra_build_content = "\n".join([ """load("@aspect_rules_js//js:defs.bzl", "js_binary")""", """js_binary( diff --git a/npm/private/test/pnpm_test.bzl b/npm/private/test/pnpm_test.bzl index b2ca1649a..2b3e71443 100644 --- a/npm/private/test/pnpm_test.bzl +++ b/npm/private/test/pnpm_test.bzl @@ -6,13 +6,15 @@ load("//npm/private:pnpm_extension.bzl", "DEFAULT_PNPM_REPO_NAME", "resolve_pnpm load("//npm/private:pnpm_repository.bzl", "DEFAULT_PNPM_VERSION", "LATEST_PNPM_VERSION") load("//npm/private:versions.bzl", "PNPM_VERSIONS") -def _fake_pnpm_tag(version = None, name = DEFAULT_PNPM_REPO_NAME, integrity = None, pnpm_version_from = None, include_npm = False): +def _fake_pnpm_tag(version = None, name = DEFAULT_PNPM_REPO_NAME, integrity = None, pnpm_version_from = None, include_npm = False, patches = [], patch_args = ["-p1"]): return struct( name = name, pnpm_version = version, pnpm_version_from = pnpm_version_from, pnpm_version_integrity = integrity, include_npm = include_npm, + patches = patches, + patch_args = patch_args, ) def _fake_mod(is_root, *pnpm_tags): @@ -41,7 +43,7 @@ def _basic(ctx): # - rules_js sets a default. return _resolve_test( ctx, - repositories = {"pnpm": {"version": "8.6.7", "integrity": "8.6.7-integrity", "include_npm": False}}, + repositories = {"pnpm": {"version": "8.6.7", "integrity": "8.6.7-integrity", "include_npm": False, "patches": [], "patch_args": ["-p1"]}}, modules = [ _fake_mod(True), _fake_mod( @@ -56,7 +58,7 @@ def _from_package_json_simple(ctx): # packageManager: "pnpm@1.2.3" -> version only, no integrity tuple return _resolve_test( ctx, - repositories = {"pnpm": {"version": "1.2.3", "integrity": None, "include_npm": False}}, + repositories = {"pnpm": {"version": "1.2.3", "integrity": None, "include_npm": False, "patches": [], "patch_args": ["-p1"]}}, modules = [ _fake_mod(True, _fake_pnpm_tag(pnpm_version_from = "//:package.json")), ], @@ -69,7 +71,7 @@ def _from_package_json_with_hash(ctx): # packageManager: "pnpm@1.2.3+sha512." -> (version, integrity) tuple return _resolve_test( ctx, - repositories = {"pnpm": {"version": "1.2.3", "integrity": "sha512-l0Ypl1YTeLb1KsXGFPOjuSOmUq1ayYcQAobkqi2EpqBkLp5F89AdMMRrErIL6w+GrreQv5qCvFnbQrZ/5p0aJQ==", "include_npm": False}}, + repositories = {"pnpm": {"version": "1.2.3", "integrity": "sha512-l0Ypl1YTeLb1KsXGFPOjuSOmUq1ayYcQAobkqi2EpqBkLp5F89AdMMRrErIL6w+GrreQv5qCvFnbQrZ/5p0aJQ==", "include_npm": False, "patches": [], "patch_args": ["-p1"]}}, modules = [ _fake_mod(True, _fake_pnpm_tag(pnpm_version_from = "//:package.json")), ], @@ -80,7 +82,7 @@ def _override(ctx): # What happens when the root overrides the pnpm version. return _resolve_test( ctx, - repositories = {"pnpm": {"version": "9.1.0", "integrity": "sha512-Z/WHmRapKT5c8FnCOFPVcb6vT3U8cH9AyyK+1fsVeMaq07bEEHzLO6CzW+AD62IaFkcayDbIe+tT+dVLtGEnJA==", "include_npm": False}}, + repositories = {"pnpm": {"version": "9.1.0", "integrity": "sha512-Z/WHmRapKT5c8FnCOFPVcb6vT3U8cH9AyyK+1fsVeMaq07bEEHzLO6CzW+AD62IaFkcayDbIe+tT+dVLtGEnJA==", "include_npm": False, "patches": [], "patch_args": ["-p1"]}}, notes = [], modules = [ _fake_mod( @@ -107,7 +109,7 @@ def _latest(ctx): # - Accept a brittle test. return _resolve_test( ctx, - repositories = {"pnpm": {"version": LATEST_PNPM_VERSION, "integrity": PNPM_VERSIONS[LATEST_PNPM_VERSION], "include_npm": False}}, + repositories = {"pnpm": {"version": LATEST_PNPM_VERSION, "integrity": PNPM_VERSIONS[LATEST_PNPM_VERSION], "include_npm": False, "patches": [], "patch_args": ["-p1"]}}, modules = [ _fake_mod(True, _fake_pnpm_tag(version = "latest")), ], @@ -117,8 +119,8 @@ def _include_npm(ctx): return _resolve_test( ctx, repositories = { - "pnpm": {"version": "9.1.0", "integrity": "sha512-Z/WHmRapKT5c8FnCOFPVcb6vT3U8cH9AyyK+1fsVeMaq07bEEHzLO6CzW+AD62IaFkcayDbIe+tT+dVLtGEnJA==", "include_npm": True}, - "wnpm": {"version": "9.2.0", "integrity": "sha512-mKgP0RwucJZ0d2IwQQZDKz3cZ9z1S1qMAck/aKLNXgXmghhJUioG+3YoTUGiZg1eM08u47vykYO/LnObHa+ncQ==", "include_npm": True}, + "pnpm": {"version": "9.1.0", "integrity": "sha512-Z/WHmRapKT5c8FnCOFPVcb6vT3U8cH9AyyK+1fsVeMaq07bEEHzLO6CzW+AD62IaFkcayDbIe+tT+dVLtGEnJA==", "include_npm": True, "patches": [], "patch_args": ["-p1"]}, + "wnpm": {"version": "9.2.0", "integrity": "sha512-mKgP0RwucJZ0d2IwQQZDKz3cZ9z1S1qMAck/aKLNXgXmghhJUioG+3YoTUGiZg1eM08u47vykYO/LnObHa+ncQ==", "include_npm": True, "patches": [], "patch_args": ["-p1"]}, }, modules = [ _fake_mod(True, _fake_pnpm_tag(version = "9.1.0", include_npm = True)), @@ -131,8 +133,8 @@ def _custom_name(ctx): return _resolve_test( ctx, repositories = { - "my-pnpm": {"version": "9.1.0", "integrity": "sha512-Z/WHmRapKT5c8FnCOFPVcb6vT3U8cH9AyyK+1fsVeMaq07bEEHzLO6CzW+AD62IaFkcayDbIe+tT+dVLtGEnJA==", "include_npm": False}, - "pnpm": {"version": "8.6.7", "integrity": "8.6.7-integrity", "include_npm": False}, + "my-pnpm": {"version": "9.1.0", "integrity": "sha512-Z/WHmRapKT5c8FnCOFPVcb6vT3U8cH9AyyK+1fsVeMaq07bEEHzLO6CzW+AD62IaFkcayDbIe+tT+dVLtGEnJA==", "include_npm": False, "patches": [], "patch_args": ["-p1"]}, + "pnpm": {"version": "8.6.7", "integrity": "8.6.7-integrity", "include_npm": False, "patches": [], "patch_args": ["-p1"]}, }, modules = [ _fake_mod( @@ -152,7 +154,7 @@ def _integrity_conflict(ctx): return _resolve_test( ctx, repositories = { - "pnpm": {"version": "8.6.7", "integrity": "dep-integrity", "include_npm": False}, + "pnpm": {"version": "8.6.7", "integrity": "dep-integrity", "include_npm": False, "patches": [], "patch_args": ["-p1"]}, }, # Modules are *BFS* from root: # https://bazel.build/rules/lib/builtins/module_ctx#modules @@ -168,6 +170,19 @@ def _integrity_conflict(ctx): ], ) +def _patch_args_empty(ctx): + # An explicit patch_args = [] must not be silently dropped in favour of ["-p1"]. + return _resolve_test( + ctx, + repositories = {"pnpm": {"version": "9.1.0", "integrity": "sha512-Z/WHmRapKT5c8FnCOFPVcb6vT3U8cH9AyyK+1fsVeMaq07bEEHzLO6CzW+AD62IaFkcayDbIe+tT+dVLtGEnJA==", "include_npm": False, "patches": ["//some:patch.patch"], "patch_args": []}}, + modules = [ + _fake_mod( + True, + _fake_pnpm_tag(version = "9.1.0", patches = ["//some:patch.patch"], patch_args = []), + ), + ], + ) + def _default_version(ctx): # Lockfile format is tied to the pnpm major. Pinning the default to v10 # keeps the lockfile format at v9; bumping the major changes the format @@ -219,6 +234,7 @@ include_npm_test = unittest.make(_include_npm) integrity_conflict_test = unittest.make(_integrity_conflict) from_package_json_simple_test = unittest.make(_from_package_json_simple) from_package_json_with_hash_test = unittest.make(_from_package_json_with_hash) +patch_args_empty_test = unittest.make(_patch_args_empty) default_version_test = unittest.make(_default_version) cpu_constraints_test = unittest.make(_cpu_constraints) os_constraints_test = unittest.make(_os_constraints) @@ -235,6 +251,7 @@ def pnpm_tests(name): integrity_conflict_test, from_package_json_simple_test, from_package_json_with_hash_test, + patch_args_empty_test, default_version_test, cpu_constraints_test, os_constraints_test,