Testing pnpm 12.11 Rust toolchain management: from rust-toolchain.toml to cargo, pnx, and global installation

Testing pnpm 12.11 Rust toolchain management: from rust-toolchain.toml to cargo, pnx, and global installation

Tested pnpm 12.11 Rust toolchain management without rustup. Covers toolchain fetch via pnpm install, offline build, nightly via pnx, global install results, and rustup coexistence.
2026.10.12

This page has been translated by machine translation. View original

Hi there! I'm Nishimura Yuji from the Operations Division!

In pnpm 12.11, pnpm itself became able to fetch and manage the Rust toolchain.

Configurations that handle both JavaScript and Rust in the same repository include napi-rs, which writes Node.js native addons in Rust, and wasm-related packages. Until now, the Rust toolchain was managed separately with rustup, the official Rust installer and version manager, while JavaScript dependencies were handled by pnpm. Starting with 12.11, fetching the Rust toolchain can also be done with pnpm install.

This article summarizes the results of running pnpm 12.11.1 in an environment without rustup or cargo, and testing toolchain fetching, offline builds, one-time execution with pnx, and global installation.

Changes in This Release

  • Target version / Date: According to the official release notes (2026-10-09), Rust toolchain management was added in 12.11.0, and pnx --package=rust@<channel> was added in 12.11.1 on the same day
  • Key changes:
    • When cargo.enabled is activated, pnpm install fetches the toolchain specified in rust-toolchain.toml
    • pnpm run and pnpm exec add cargo and rustc from the fetched toolchain to PATH
    • pnpm add rust@<channel> pins the toolchain in rust-toolchain.toml, and pnpm add -g rust@<channel> installs it globally
    • In 12.11.1, pnx, which fetches a package temporarily and runs just one command, can launch tools from a different Rust release by specifying --package=rust@<channel>
  • Official links: pnpm 12.11.0 release notes / pnpm 12.11.1 release notes / cargo integration documentation

Test Environment

  • A disposable Docker container (Ubuntu 24.04.5 LTS, aarch64)
  • Node.js v24.21.0
  • pnpm 12.11.1 (installed to the working directory with npm install --prefix)
  • rustup, cargo, rustc, and a C linker (cc) were not installed

Fetching the Rust Toolchain with pnpm install

Enabling and the Project Used

Rust toolchain management is a feature that operates under cargo.enabled, and it is enabled in pnpm-workspace.yaml. The storeDir is specified to keep the store within the working directory for the disposable container test environment, and is not normally required.

pnpm-workspace.yaml
storeDir: /work/store
cargo:
  enabled: true

According to the documentation, pnpm reads rust-toolchain.toml (or the legacy rust-toolchain file) the same way rustup does. Each Cargo manifest uses the closest toolchain file found in the same directory or a parent directory within the repository. The fields that can be handled are as follows. Cases where pnpm does not handle something and leaves it to rustup are summarized later in "Division of Responsibilities with rustup."

  • channel: version (1.95.0 or 1.95), stable, beta, nightly, or date-specified nightly
  • profile: minimal (rustc and cargo, standard library) or default (adds rustfmt and clippy)
  • components / targets: additional components and standard libraries for other targets

I first tested with the following rust-toolchain.toml without specifying targets.

rust-toolchain.toml
[toolchain]
channel = "1.95.0"
profile = "minimal"

On the Rust side, it's a program that simply prints a string using one crate. I left [dependencies] in Cargo.toml empty and added it later with pnpm add crate:itoa.

Cargo.toml
[package]
name = "demo"
version = "0.1.0"
edition = "2021"

[dependencies]
src/main.rs
fn main() { let mut b = itoa::Buffer::new(); println!("hello from {}", b.format(2026)); }

In package.json, I wrote scripts to call cargo from pnpm run.

package.json
{ "name": "demo", "private": true, "scripts": { "build": "cargo build", "ver": "cargo --version && rustc --version" } }

Fetching the Toolchain

When I first ran pnpm add crate:itoa (adding a crate is described later), it displayed Installing Rust 1.95.0 (aarch64-unknown-linux-gnu) during the process, and completed including the toolchain fetch in 48.6 seconds. The subsequent pnpm install returned Already up to date in 116ms, and no re-download occurred.

Screen showing pnpm install fetching Rust 1.95.0 and pnpm exec displaying the versions of cargo and rustc in an environment without rustup

This screenshot shows the output saved to a file from running pnpm install (42.3 seconds) from a separate empty store, using the rust-toolchain.toml with targets added as described later.

The fetched toolchain is stored under rust in the store, and .pnpm/rust in the project was a symbolic link pointing there.

.pnpm/rust -> ../../store/v11/rust/1.95.0-aarch64-unknown-linux-gnu-d9ba01ca22a2e217

According to the documentation, pnpm verifies the release manifest with Rust's release signing key and checks each download against the hash listed in the manifest. Moving channels like stable are checked once a day, and when offline or unreachable, the latest release available in the store is used. Pinning a version skips this check.

cargo and rustc Are Added to PATH

According to the documentation, pnpm run, pnpm exec, and the cargo commands that pnpm runs during installation all use the linked toolchain. In my environment as well, with no cargo on PATH, both pnpm exec cargo --version and pnpm run ver returned cargo 1.95.0 (f2d3ce0bd 2026-03-21). Running command -v cargo inside pnpm exec showed bin/cargo inside the store.

$ pnpm exec sh -c 'command -v cargo'
/work/store/v11/rust/1.95.0-aarch64-unknown-linux-gnu-d9ba01ca22a2e217/bin/cargo

Crate Vendoring and Offline Builds

Next, I'll verify whether actual builds work with the fetched toolchain. The mechanism for managing crates with pnpm is an existing feature of cargo integration (v12.4.0~) and is not a new feature in 12.11. In 12.11.0, the generation of .pnpm/crates/config.toml and the optional [[include]] entry mentioned later were also changed.

What Was Added by pnpm add crate:itoa

Running pnpm add crate:itoa wrote itoa = "1.0.18" into [dependencies] in Cargo.toml, and added the following content to .cargo/config.toml.

.cargo/config.toml
# >>> pnpm-managed cargo sources >>>
[[include]]
path = "../.pnpm/crates/config.toml"
optional = true
# <<< pnpm-managed cargo sources <<<

The included .pnpm/crates/config.toml contained settings that replace crates.io with a directory source at .pnpm/crates/crates-io.

.pnpm/crates/config.toml
[source.crates-io]
replace-with = "pnpm-crates-io"

[source.pnpm-crates-io]
directory = "crates/crates-io"

According to the documentation, pnpm itself does not compile anything, and the [[include]] above is read by Cargo 1.95 and later. For a fresh clone without .pnpm, Cargo skips this include and fetches from the registry as usual. Crates from Git sources are vendored to .pnpm/crates/git. Crate information is recorded only in Cargo.lock, and in my environment, pnpm-lock.yaml had no entry for itoa.

Building Without a Network

In a container with the network disabled, I first ran pnpm run build (which runs cargo build) targeting the host (aarch64-unknown-linux-gnu). It progressed as far as compiling itoa, but failed with error: linker `cc` not found. This was because the test environment has no C linker installed.

So I decided to verify using wasm32-wasip1, which does not use cc for linking, and added targets to rust-toolchain.toml.

rust-toolchain.toml
[toolchain]
channel = "1.95.0"
profile = "minimal"
targets = ["wasm32-wasip1"]

Running pnpm install in this state triggered Installing Rust 1.95.0 again (41.7 seconds). A new directory was created in the store for the same 1.95.0 (1.95.0-aarch64-unknown-linux-gnu-46aff0e70cf46ac2), and the .pnpm/rust symlink was updated to point there. Changing targets does not add the target to the existing toolchain; instead, it adds a new toolchain entry to the store.

Before adding targets, the store was 513MB with one toolchain and one crate. When estimating store capacity, keep in mind that a new toolchain is added for each combination of targets and components.

I also updated the build script for wasm32-wasip1.

package.json
{ "name": "demo", "private": true, "scripts": { "build": "cargo build --target wasm32-wasip1", "ver": "cargo --version && rustc --version" } }

The resulting Wasm is run with Node.js's WASI.

run-wasi.mjs
import { readFile } from 'node:fs/promises';
import { WASI } from 'node:wasi';
const wasi = new WASI({ version: 'preview1' });
const { instance } = await WebAssembly.instantiate(await readFile(process.argv[2]), wasi.getImportObject());
wasi.start(instance);
pnpm run build
node --no-warnings run-wasi.mjs target/wasm32-wasip1/debug/demo.wasm

Even with index.crates.io name resolution unavailable, itoa was compiled, and running the Wasm displayed hello from 2026.

Screen showing pnpm run build compiling itoa without a network and running the Wasm with Node.js

Pinning, One-Time Execution, and Global Installation of Toolchains

Pinning a Toolchain to a Project

Running pnpm add rust@<channel> inside a project pins the toolchain in the applicable rust-toolchain.toml. Here rust refers to the Rust toolchain, not an npm package of that name.

pnpm add rust@1.95

Running this displayed Pinned Rust 1.95 in /work/demo/rust-toolchain.toml, and the only change was one line where channel = "1.95.0" became channel = "1.95". package.json was unchanged.

Running a Single Command with a Different Release (12.11.1)

pnx --package=rust@<channel> <tool>, added in 12.11.1, launches a tool from that Rust release. I tested with the same date-specific nightly as in the release notes example.

$ pnx --package=rust@nightly-2026-01-01 cargo --version
Installing Rust nightly-2026-01-01 (aarch64-unknown-linux-gnu)
cargo 1.94.0-nightly (b54051b15 2025-12-30)
Done in 53.5s using pnpm v12.11.1

Running rustc --version for the same nightly afterward used the saved one and returned in 97ms. After execution, pnpm exec rustc --version in the project still showed 1.95.0, and rust-toolchain.toml was unchanged.

According to the release notes, it installs that release with the components and targets from rust-toolchain.toml, plus the targets for each --target argument. In my environment as well, the installed nightly included the standard library for wasm32-wasip1. This allows trying nightly and other releases just once without changing the regular toolchain.

Installing Globally

pnpm add -g rust@<channel> installs the toolchain globally. This time I set PNPM_HOME, added $PNPM_HOME/bin to PATH, and then ran it (the store is pointed to a directory within the working folder via the pnpm_config_store_dir environment variable).

export PNPM_HOME=/work/pnpm-home
export PATH=$PNPM_HOME/bin:$PNPM_HOME:$PATH
pnpm add -g rust@1.94.0

It displayed Installed Rust 1.94.0 globally, and cargo, rustc, rustfmt, clippy-driver, and others were placed in $PNPM_HOME/bin.

$ cd /tmp && cargo --version
cargo 1.94.0 (85eff7c80 2026-01-15)
$ cd /work/demo && cargo --version
cargo 1.95.0 (f2d3ce0bd 2026-03-21)

Outside the project, the plain cargo uses the global 1.94.0, and inside the project with rust-toolchain.toml, it uses 1.95.0 — switching automatically by location. pnpm ls -g also showed rust@1.94.0.

Division of Responsibilities with rustup

Based on the documentation and my test results, the division between pnpm and rustup can be summarized as follows.

Aspect Handled by pnpm Handled by rustup
Toolchain file Those specifying channel, profile (minimal / default), components, and targets path specification, no channel, toolchains registered with rustup toolchain link, other profiles like complete, machines for which Rust does not publish a toolchain
Switching plain cargo and rustc Install a global toolchain with pnpm add -g rust@<channel> (confirmed this time) On machines with rustup, pnpm shim add rust reportedly achieves the same without a global toolchain

pnpm shim is a subcommand that manages shims for commands that invoke the version pinned by the current project. pnpm shim add rust is a feature for rustup environments. I did not verify its behavior this time, as I did not prepare a rustup environment for comparison.

pnpm shim add rust

Disabling Only Toolchain Management

The documentation states the following.

runtimeOnFail set to error, warn, or ignore turns this off and leaves the toolchain to rustup.

Source: Cargo dependencies | pnpm

Setting runtimeOnFail to error, warn, or ignore reportedly disables pnpm's toolchain management and leaves the toolchain to rustup. I only tested ignore myself.

Running pnpm install in a new project with runtimeOnFail: ignore set did not display Installing Rust, and pnpm install failed at the run cargo metadata for manifest discovery stage.

Error:   × run cargo metadata for manifest discovery
  ╰─▶ No such file or directory (os error 2)

If you want to fetch the toolchain from a mirror, you can configure tools.rust.mirror (not verified).

How to Handle This in Practice

  • Worth trying: When you are managing JavaScript/TypeScript and Rust in a single pnpm workspace and want to consolidate Rust toolchain setup into pnpm install. Even in environments without rustup, pnpm install alone was enough to make cargo available.
  • Fine to wait and see: cargo.enabled is experimental and disabled by default. Before rolling it out to CI or a shared repository all at once, it's a good idea to verify the behavior in a temporary project first.
  • Things to decide as a team: The channel pinning policy in rust-toolchain.toml, whether to use rustup alongside pnpm, how to provide the linker required for native builds, and how to handle CI caching that depends on .pnpm/crates and the store.

What Was Not Verified

  • pnpm shim add rust (a feature for rustup environments; no rustup environment was prepared for comparison)
  • runtimeOnFail behavior with error and warn, and rustup taking over the toolchain after setting ignore
  • Cases where signature and hash verification fails, and the behavior of once-a-day checks for moving channels and offline situations
  • tools.rust.mirror, Git source crates, and building from a clone without .pnpm
  • Target addition via the --target argument of pnx
  • Behavior on macOS / Windows / x86_64, and coexistence with an existing rustup environment
  • Re-verification with pnpm 12.11.2 / 12.12.0 (this verification used 12.11.1)

Summary

I tested pnpm 12.11.1 in an environment without rustup or cargo. Under cargo.enabled, pnpm install fetched the toolchain from rust-toolchain.toml, and cargo and rustc were available from pnpm run and pnpm exec.

Combined with crate vendoring, a wasm32-wasip1 build succeeded even without a network. A linker for native builds must be provided separately, and changing targets adds a new toolchain entry to the store.

With pnx --package=rust@<channel> in 12.11.1, I was able to use nightly for just one command without changing the project's toolchain. After installing globally with pnpm add -g rust@<channel>, the plain cargo automatically switched to the global toolchain outside the project and to the rust-toolchain.toml toolchain inside the project.

As for the division of responsibilities with rustup, pnpm handles ordinary toolchain files that specify channel and other fields, while path specifications and linked toolchains are left to rustup. It seems worth trying as an option for managing the toolchain with pnpm in repositories that mix JavaScript and Rust.

I hope this is helpful to someone.


Reference links:

Share this article

Related articles