pnpm 12.11のRust toolchain管理を試す:rust-toolchain.tomlからcargo、pnx、グローバル導入まで

pnpm 12.11のRust toolchain管理を試す:rust-toolchain.tomlからcargo、pnx、グローバル導入まで

pnpm 12.11で入ったRust toolchain管理を、rustupのない環境で試しました。pnpm installによるtoolchainの取得、オフラインビルド、pnxでのnightly起動、グローバル導入の結果と、rustupとの分担をまとめます。
2026.10.12

どうも!オペ部の西村祐二です!

pnpmの12.11で、pnpm自身がRust toolchainを取得して管理できるようになりました。

JavaScriptとRustを同じリポジトリで扱う構成には、Node.jsのネイティブアドオンをRustで書くnapi-rsや、wasm系のパッケージなどがあります。これまでは、Rust toolchainはRust公式のインストーラー兼バージョン管理ツールであるrustupで、JavaScriptの依存はpnpmで、と別々に用意していました。12.11からは、Rust toolchainの取得もpnpm installで済ませられます。

この記事では、rustupもcargoも入っていない環境でpnpm 12.11.1を動かし、toolchainの取得、オフラインビルド、pnxでの一時起動、グローバル導入を試した結果をまとめます。

今回の変更点

  • 対象バージョン / 日付: 公式リリースノート(2026-10-09)によると、12.11.0でRust toolchain管理が、同日の12.11.1でpnx --package=rust@<channel>が追加されています
  • 変更の要点:
    • cargo.enabledを有効にすると、pnpm installがrust-toolchain.tomlに書かれたtoolchainを取得する
    • pnpm runとpnpm execが、取得したtoolchainのcargoとrustcをPATHに載せる
    • pnpm add rust@<channel>でtoolchainをrust-toolchain.tomlに固定し、pnpm add -g rust@<channel>でグローバルに導入できる
    • 12.11.1では、パッケージを一時的に取得して1コマンドだけ実行するpnxで、--package=rust@<channel>を指定すると別のRustリリースのツールを起動できる
  • 公式リンク: pnpm 12.11.0 リリースノート / pnpm 12.11.1 リリースノート / cargo連携ドキュメント

検証環境

  • 使い捨てのDockerコンテナ(Ubuntu 24.04.5 LTS、aarch64)
  • Node.js v24.21.0
  • pnpm 12.11.1(npm install --prefixで作業フォルダに導入)
  • rustup、cargo、rustc、Cリンカ(cc)は入っていない状態

pnpm installでRust toolchainを取得する

有効化と試したプロジェクト

Rust toolchain管理はcargo.enabledの下で動く機能で、有効化はpnpm-workspace.yamlで行います。storeDirは、コンテナを毎回破棄する検証環境の都合でストアを作業フォルダ内に置くために指定したもので、通常は不要です。

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

ドキュメントによると、pnpmはrust-toolchain.toml(または旧形式のrust-toolchainファイル)をrustupと同じように読むとされています。各Cargoマニフェストは、リポジトリ内で同じディレクトリか、それより上の階層にある最も近いtoolchainファイルを使います。扱えるフィールドは次のとおりです。pnpmが扱わずrustupに任せるケースは、後述の「rustupとの分担」でまとめます。

  • channel: バージョン(1.95.0や1.95)、stable、beta、nightly、日付付きのnightly
  • profile: minimal(rustcとcargo、標準ライブラリ)またはdefault(rustfmtとclippyを追加)
  • components / targets: 追加のコンポーネントと、ほかのターゲット向けの標準ライブラリ

最初はtargetsを書かない、次のrust-toolchain.tomlで試しました。

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

Rust側は、crateを1つ使って文字列を表示するだけのプログラムです。Cargo.tomlの[dependencies]は空にしておき、後で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)); }

package.jsonには、pnpm runからcargoを呼ぶスクリプトを書きました。

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

toolchainの取得

最初にpnpm add crate:itoa(crateの追加は後述)を実行すると、その中でInstalling Rust 1.95.0 (aarch64-unknown-linux-gnu)と表示され、toolchainの取得を含めて48.6秒で完了しました。続けて実行したpnpm installは116msでAlready up to dateとなり、再ダウンロードは起きませんでした。

rustupのない環境でpnpm installがRust 1.95.0を取得し、pnpm execでcargoとrustcのバージョンを表示した画面

この画面は、後述のtargetsを加えたrust-toolchain.tomlで、別の空のストアからpnpm installを実行したとき(42.3秒)の出力をファイルに保存してから表示したものです。

取得したtoolchainはストアのrust以下に保存され、プロジェクトの.pnpm/rustはそこへのシンボリックリンクになっていました。

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

ドキュメントによると、pnpmはリリースマニフェストをRustのリリース署名鍵で検証し、各ダウンロードをマニフェスト記載のハッシュと照合するとされています。stableのような動くチャンネルは1日1回チェックされ、オフラインのときやサーバーに届かないときは、ストアにある最新のリリースが使われるとされています。バージョンを固定すると、このチェックは行われないとされています。

cargoとrustcがPATHに載る

ドキュメントによると、pnpm runとpnpm exec、pnpmがインストール中に走らせるcargoコマンドは、リンクされたtoolchainを使うとされています。手元でも、PATH上にcargoがない状態で、pnpm exec cargo --versionとpnpm run verの両方がcargo 1.95.0 (f2d3ce0bd 2026-03-21)を返しました。pnpm execの中でcommand -v cargoを実行すると、ストア内のbin/cargoが見えています。

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

crateのvendoringとオフラインビルド

ここからは、取得したtoolchainで実際にビルドできるかを確かめます。crateをpnpmで扱う仕組みは、cargo連携(v12.4.0〜)の既存機能で、12.11の新機能ではありません。12.11.0では、このあと出てくる.pnpm/crates/config.tomlとoptionalな[[include]]の書き出しも変更されています。

pnpm add crate:itoaで追加されたもの

pnpm add crate:itoaを実行すると、Cargo.tomlの[dependencies]にitoa = "1.0.18"が書き込まれ、.cargo/config.tomlには次の内容が追加されました。

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

include先の.pnpm/crates/config.tomlには、crates.ioを.pnpm/crates/crates-ioのディレクトリソースへ置き換える設定が書かれていました。

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

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

ドキュメントによると、pnpm自身は何もコンパイルせず、先ほどの[[include]]はCargo 1.95以降が読むとされています。.pnpmのない新規cloneでは、Cargoはこのincludeを飛ばして通常どおりレジストリから取得するとされています。Gitソースのcrateは.pnpm/crates/gitへvendorされるとされています。crateの情報はCargo.lockだけに記録され、手元でもpnpm-lock.yamlにはitoaのエントリーがありませんでした。

ネットワークなしでビルドする

ネットワークを切ったコンテナで、まずホスト向け(aarch64-unknown-linux-gnu)にpnpm run build(中身はcargo build)を実行しました。itoaのコンパイルまでは進みましたが、error: linker `cc` not foundで失敗しました。検証環境にCリンカが入っていないためです。

そこで、リンクにccを使わないwasm32-wasip1向けで確認することにし、rust-toolchain.tomlにtargetsを追加しました。

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

この状態でpnpm installを実行すると、Installing Rust 1.95.0がもう一度走りました(41.7秒)。ストアには同じ1.95.0の別ディレクトリ(1.95.0-aarch64-unknown-linux-gnu-46aff0e70cf46ac2)が作られ、.pnpm/rustのリンク先もそちらに変わりました。targetsを変えた構成は、既存のtoolchainにターゲットを足すのではなく、別のtoolchainとしてストアに追加されます。

targetsを追加する前の時点で、ストアはtoolchain 1つとcrate 1つで513MBでした。ストアの容量を見積もるときは、targetsやcomponentsの組み合わせごとにtoolchainが増える点を考えておく必要があります。

ビルドスクリプトもwasm32-wasip1向けに変えました。

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

できたWasmは、Node.jsの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

index.crates.ioの名前解決ができない状態でもitoaがコンパイルされ、Wasmを実行するとhello from 2026が表示されました。

ネットワークのない状態でpnpm run buildがitoaをコンパイルし、WasmをNode.jsで実行した画面

toolchainの固定・一時起動・グローバル導入

toolchainをプロジェクトに固定する

プロジェクト内でpnpm add rust@<channel>を実行すると、適用されるrust-toolchain.tomlにtoolchainが固定されます。rustはその名前のnpmパッケージではなく、Rust toolchainを指します。

pnpm add rust@1.95

実行するとPinned Rust 1.95 in /work/demo/rust-toolchain.tomlと表示され、変更はchannel = "1.95.0"がchannel = "1.95"になった1行だけでした。package.jsonは変わっていません。

1コマンドだけ別のリリースで動かす(12.11.1)

12.11.1で追加されたpnx --package=rust@<channel> <tool>は、そのRustリリースのツールを起動します。リリースノートの例と同じ日付のnightlyで試しました。

$ 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

続けて同じnightlyのrustc --versionを実行すると、保存済みのものが使われて97msで返りました。実行後もプロジェクトのpnpm exec rustc --versionは1.95.0のままで、rust-toolchain.tomlも変わっていません。

リリースノートでは、rust-toolchain.tomlのcomponentsとtargets、さらに各--target引数のターゲットを付けてそのリリースを導入するとされています。手元でも、導入されたnightlyにwasm32-wasip1の標準ライブラリが入っていました。普段のtoolchainを変えずに、nightlyなどを一度だけ試せます。

グローバルに導入する

pnpm add -g rust@<channel>は、toolchainをグローバルに導入します。今回はPNPM_HOMEを設定し、$PNPM_HOME/binをPATHに追加してから実行しました(ストアは環境変数pnpm_config_store_dirで作業フォルダ内を指定しています)。

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

Installed Rust 1.94.0 globallyと表示され、$PNPM_HOME/binにcargo、rustc、rustfmt、clippy-driverなどが置かれました。

$ 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)

プロジェクトの外ではグローバルの1.94.0、rust-toolchain.tomlのあるプロジェクト内では1.95.0と、素のcargoが場所によって使い分けられています。pnpm ls -gにもrust@1.94.0が表示されました。

rustupとの分担

ドキュメントの記載と手元の結果から、pnpmとrustupの分担を整理すると次のようになります。

観点 pnpmが扱う rustup側
toolchainファイル channel、profile(minimal / default)、components、targetsを指定したもの path指定、channelなし、rustup toolchain linkで登録したtoolchain、completeなど別のprofile、Rustが公開toolchainを出していないマシン
素のcargoとrustcの切り替え pnpm add -g rust@<channel>でグローバルtoolchainを導入する(今回確認) rustupがあるマシンでは、pnpm shim add rustでグローバルtoolchainなしに同じことができるとされています

pnpm shimは、いまいるプロジェクトが固定したバージョンを呼び出すコマンドのshimを管理するサブコマンドです。pnpm shim add rustはrustup環境向けの機能です。今回は比較対象のrustup環境を用意していないため、動作は確認していません。

pnpm shim add rust

toolchain管理だけを切る場合

ドキュメントには、次のように書かれています。

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

出典: Cargo dependencies | pnpm

runtimeOnFailをerror、warn、ignoreのいずれかにすると、pnpmのtoolchain管理はオフになり、toolchainはrustupに任されるとされています。手元で試したのはignoreだけです。

runtimeOnFail: ignoreを設定した新規プロジェクトでpnpm installを実行すると、Installing Rustは表示されず、run cargo metadata for manifest discoveryの段階でpnpm installが失敗しました。

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

ミラーからtoolchainを取得したい場合は、tools.rust.mirrorを設定できるとされています(未確認)。

実務でどう扱うか

  • 試す価値がある場合: JavaScript/TypeScriptとRustを1つのpnpmワークスペースで扱っていて、Rust toolchainの用意もpnpm installに寄せたいとき。rustupのない環境でも、pnpm installだけでcargoが使える状態になりました。
  • 様子見でよい場合: cargo.enabledは実験的で既定無効です。CIや共有リポジトリへ一気に入れる前に、一時プロジェクトで挙動を押さえておくのが良さそうです。
  • チームで決めておきたいこと: rust-toolchain.tomlのchannel固定方針、rustup併用の有無、ネイティブビルドに必要なリンカの用意、.pnpm/cratesやストアを前提にしたCIキャッシュの扱い。

確認していない範囲

  • pnpm shim add rust(rustup環境向けの機能で、今回は比較対象のrustup環境を用意していないため)
  • runtimeOnFailのerrorとwarn、ignoreにしたあとrustupがtoolchainを引き継ぐ挙動
  • 署名とハッシュの検証が失敗するケース、動くチャンネルの1日1回チェックとオフライン時の挙動
  • tools.rust.mirror、Gitソースのcrate、.pnpmのないcloneでのビルド
  • pnxの--target引数によるターゲット追加
  • macOS / Windows / x86_64での挙動、既存rustup環境との共存
  • pnpm 12.11.2 / 12.12.0での再確認(今回の検証は12.11.1)

まとめ

pnpm 12.11.1を、rustupもcargoもない環境で試しました。cargo.enabledの下でrust-toolchain.tomlのtoolchainをpnpm installが取得し、pnpm runとpnpm execからcargoとrustcが使えました。

crateのvendoringと組み合わせると、ネットワークのない状態でもwasm32-wasip1向けのビルドが通りました。ネイティブビルドのリンカは別途必要で、targetsを変えるとtoolchainがストアに追加されます。

12.11.1のpnx --package=rust@<channel>では、プロジェクトのtoolchainを変えずにnightlyを1コマンドだけ使えました。pnpm add -g rust@<channel>でグローバルに導入すると、素のcargoがプロジェクトの外ではグローバルのtoolchain、プロジェクト内ではrust-toolchain.tomlのtoolchainに切り替わりました。

rustupとの分担では、pnpmが扱うのはchannelなどを指定した一般的なtoolchainファイルで、path指定やリンク済みtoolchainなどはrustupに任せる形です。JavaScriptとRustが混在するリポジトリで、toolchainの管理をpnpmに寄せる選択肢として試してみる価値はありそうです。

誰かの参考になれば幸いです。


参考リンク:

この記事をシェアする

関連記事