RFC 0001: Consume pnpm lockfiles as hermetic build inputs #12
Windsor Nguyễn (windsornguyen)
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
RFC: Consume pnpm lockfiles as hermetic build inputs
Status: Proposed
Summary
BSMR should integrate with pnpm rather than replace it.
pnpm should remain responsible for npm ecosystem semantics: interpreting
manifests, resolving versions and peer dependencies, applying overrides and
catalogs, and writing
pnpm-lock.yaml. BSMR should consume the committedlockfile as an immutable dependency graph, fetch and verify the selected
artifacts, materialize a pnpm-compatible dependency layout, and execute approved
package build steps as normal BSMR actions.
This design is successful only if it improves the outcomes developers actually
care about:
sound, target-platform dependencies are selected correctly, and unsupported
cases fail instead of silently changing semantics.
contents are shared through the CAS, and dependency build outputs can be
reused locally or remotely.
actions run without network access or ambient host state, and lifecycle
scripts execute only under explicit project policy.
package.json,pnpm-workspace.yaml, andpnpm-lock.yamlfiles. BSMR must not require asecond dependency manifest or a BSMR-specific version resolver.
The intended boundary is similar to BSMR's existing Conan integration and
Aspect's
rules_js: the ecosystem package manager owns dependency resolution;the build system owns the frozen build graph and its execution.
Decision
The proposed architecture has three explicit phases:
We should describe this publicly as hermetic frozen builds, not "hermetic
dependency resolution." Updating a lockfile necessarily consults registry state.
Once the lockfile is committed, neither dependency resolution nor public network
access belongs in the build.
User experience
The normal workflow should remain familiar:
package.jsonor uses pnpm to add or update a dependency.pnpm-lock.yaml.A stale or unsupported lockfile must produce one actionable error explaining
which file or package is invalid and which explicit command updates it. BSMR
must never repair or rewrite the lockfile during an ordinary build.
Requirements
Each requirement below is intended to become an independently reviewable
implementation issue. The "not desired" sections are part of the contract, not
optional implementation advice.
1. Pin pnpm as a verified project toolchain
Desired
declaration.
to another.
Not desired
latest,latest-11, or other registry tag.pnpmhappens to appear first onPATH.declared toolchain is unavailable.
Acceptance
pnpm installation.
version and a direct remediation.
2. Separate resolution, fetching, and frozen builds
Desired
build.
changing it.
the CAS.
Not desired
pnpm installin update mode as an implicit build step.Acceptance
phase has completed.
a different resolution.
3. Validate the pnpm lockfile fail-closed
Desired
inputs.
Not desired
identity.
Acceptance
integrity, invalid integrity, patches, aliases, overrides, catalogs, Git
sources, HTTP tarballs, and workspace references.
invocation without weakening fail-closed behavior.
4. Import a canonical, lossless Node dependency graph
Desired
optionality, target constraints, and workspace relationships.
and version match.
provenance, policy checks, and affected-target analysis.
Not desired
target.
Acceptance
edges as the pinned pnpm version.
and which peer context distinguishes it.
identity; changing a semantic input does.
5. Fetch immutable package artifacts into the CAS
Desired
digest.
and cache keys while still binding the requested registry and artifact
identity into the graph.
Not desired
node_modulesarchive as the primitive.Acceptance
registry.
bytes and packages.
6. Select the target dependency closure
Desired
os,cpu, libc, engine, and optional-dependency constraintsagainst the declared target and execution platforms.
Not desired
Acceptance
select the same closure as the pinned pnpm contract.
client host.
included or excluded each conditional package.
7. Materialize a deterministic pnpm-compatible layout
Desired
aliases, scoped packages, and executable shims.
Not desired
release.
node_modules/.pnpmdirectory among concurrent actions.same semantics.
Acceptance
.binbehavior,workspace links, and peer variants against pinned pnpm with hoisting disabled.
8. Model approved lifecycle hooks as build actions
Desired
possible.
toolchains, environment, and target platform as inputs.
locally and remotely.
undeclared downloads.
Not desired
preinstall,install, orpostinstallscripts merelybecause a package contains them.
--ignore-scriptsand pretending packages requiring buildsare complete.
environment.
fails.
environments.
Acceptance
denied hooks, network attempts, undeclared writes, failures, and
platform-specific outputs.
machine.
policy entry required for review.
9. Expose npm packages as first-class BSMR dependencies
Desired
duplicating their versions or transitive edges in BUCK files.
materialization.
Not desired
pnpm installprocess per workspace package.Acceptance
and use it from a BSMR target without copying version data.
still has the dependency transitively.
declared dependency order.
10. Make cache identity complete and explainable
Desired
target and execution platforms, declared environment, materializer version,
and relevant BSMR configuration in action identity.
workspace build results at the narrowest useful reusable boundary.
Not desired
pnpm-lock.yaml.match.
Acceptance
actions.
11. Provide provenance, policy, and diagnostics from the same graph
Desired
platform decision, lifecycle policy, toolchains, and producing action for each
package artifact.
without re-scanning an opaque
node_modulestree.rebuilt?" explanations.
Not desired
Acceptance
lockfile snapshot, fetched bytes, lifecycle output, and consuming target.
packages.
12. Prove compatibility and performance before defaulting to native materialization
Desired
version.
overrides, catalogs, patches, executable shims, lifecycle hooks, native
addons, Git and HTTP sources, and all supported platforms.
remote-cache restore on representative monorepos.
Not desired
clearly and documented migrations exist.
Acceptance
resolution, executable links, and lifecycle outputs.
dependency closure, and correctness results.
Delivery milestones
Milestone 0: establish the pinned adapter baseline
Invoke one exact, verified pnpm per sandbox with a frozen lockfile. Capture
correctness, closure, and performance baselines. Parallelize BSMR workspace build
actions above pnpm rather than spawning one installer per package.
Exit criterion: a documented, tested baseline with no floating toolchain or
mutable lock behavior.
Milestone 1: import and validate the lockfile
Add a read-only pnpm lockfile importer and canonical graph. Use it for queries,
policy, provenance, target closure analysis, and mismatch diagnostics while
pnpm still performs materialization.
Exit criterion: differential graph tests pass for the initial supported schema
and the importer changes no build behavior.
Milestone 2: own fetching and CAS storage
Fetch verified package artifacts as individual CAS objects. Continue using the
pinned pnpm-compatible path for final materialization while validating that the
fetched closure is complete.
Exit criterion: a populated CAS supports offline frozen installation and every
artifact has verified provenance.
Milestone 3: materialize packages without pnpm
Implement strict, non-hoisted linking for packages that do not require lifecycle
hooks. Compare results continuously with pinned pnpm.
Exit criterion: supported hook-free fixtures and representative repositories
pass module-resolution and build tests without invoking pnpm during the frozen
build.
Milestone 4: actionize lifecycle hooks
Build explicitly approved dependency packages as sandboxed, cacheable,
platform-aware actions.
Exit criterion: supported native and generated-package fixtures reproduce across
clean machines and remote execution without public network access.
Milestone 5: cut over
Make BSMR-native lock consumption the only frozen-build path for the supported
surface. Keep pnpm solely for explicit lockfile resolution and updates.
Exit criterion: compatibility, security, performance, documentation, and
migration gates pass; unsupported behavior fails before execution; no automatic
fallback remains.
Initial scope
The first production-quality release should support the pnpm schema and feature
surface required by a pinned set of representative BSMR workspaces. At minimum,
that surface should include:
workspace:referencesSupport must be defined by conformance fixtures, not by accepting arbitrary
syntax optimistically.
Non-goals
release.
pnpm installprocesses.randomness, absolute paths, or undeclared host information. Hermeticity is
necessary but not sufficient for reproducibility.
Alternatives considered
Shell out to pnpm forever
This is the correct baseline and may remain sufficient for small repositories.
It does not expose package-level graph, provenance, policy, or caching boundaries
to BSMR, and it requires treating installation as one comparatively opaque
action.
Reimplement pnpm resolution
This adds a large compatibility surface without improving the initial developer
workflow. BSMR would own semver selection, peer resolution, registries,
overrides, catalogs, patches, workspace semantics, and years of ecosystem edge
cases. That work should occur only if a measured product requirement cannot be
met through the lockfile contract.
Depend on Pacquet as the primary package manager
A Rust implementation may eventually reduce integration cost, but language
choice does not establish compatibility, hermeticity, or performance. Pacquet
can be evaluated independently without making BSMR's architecture depend on its
release cadence or parity status.
Adopt
rules_jsdirectlyrules_jsis the strongest public precedent for this design, but it is builtfor Bazel's repository and rule APIs. BSMR should adopt the proven boundary and
conformance lessons while implementing the smallest native integration that
fits BSMR's graph, CAS, platforms, and execution model.
Risks
versioned parsers, fixtures, and intentional migrations.
pnpm implementation.
network denial, and complete action keys.
and execution platforms rather than the client host.
sensitivity, and read-only package behavior on every supported platform.
materialization overhead at large workspace scale before fixing granularity.
diagnostics until BSMR's materializer is the single frozen-build path.
Success criteria
This RFC is complete when a developer can:
fetched.
build it, and why an action was or was not cached.
untrusted dependency case.
The project should optimize for these outcomes, not for ownership of package
manager internals.
Prior art and references
Conan integration
already separates package-manager resolution from BSMR-controlled package
actions.
rules_jspnpm integrationtranslates
pnpm-lock.yaml, fetches packages through Bazel's downloader,creates pnpm-compatible links, and models lifecycle hooks as actions.
pnpm fetchas a lockfile-driven acquisitionphase suitable for separating dependency fetching from later installation.
package manifest documentation defines its
standard package-manager pinning and dependency policy fields.
Questions for design review
These questions should be settled before their respective implementation
milestones; they do not reopen the core boundary established by this RFC.
invocation of the project-pinned pnpm?
BSMR-specific policy?
duplicating dependency declarations in BUCK files?
conformance and performance corpus?
All reactions