私は Rust が好きで、かなり前からです。その利点は明らかで、欠点も同様にはっきりしています。メモリ Safe が GC を必要とせず、零成本抽象、并发安全、ほぼ現段階で最高のツールチェーンと配套外围建设があり、広範なカバレッジ(Web 云服务器から単片機まで)を誇り、エラーハンドリング設計も非常に優れています。欠点としては、学習曲線が急で、Ownership、Borrow などのモデル制約を理解しにくいこと、そしてコードを書くのが遅いことですか?
長い間、私たちはこれを Rust の欠点だと考えてきましたが、実は最初の二つは思考と認知を鍛えることで解決できます。しかし、コンパイル速度の遅さは私の作業フローをずっと遅くしています。数年前まではこれでも良かったのですが、AI が席巻する前は忍耐力があり、アルゴリズムや機能を深夜まで書き続けていました。しかし今はどうでしょうか?変わらなかったとは言い難く、全く忍耐力がなくなり、Rust のコードを書き方を考える気にもなれず、Cargo の長い進行バーを待つことさえできません。特に ld の段階で進捗が表示されずに止まってしまうのは本当に辛いです。
これらすべてに対処する方法は本当にないのでしょうか?実はあるのです。約1か月前、私は Rust の開発フローを速くするためにワークフローの最適化などいくつかの方法を試してみましたが、実際の待ち時間は毎回 Cargo で止まっていました。これを解決できなければ根本的な解決にはなりません。そこでちょうど良い機会ですので、Rust 開発における Cargo.toml の設定チューニングに関する内容と、その周辺設定について少しお話ししようと思います。
バックエンド理論
Rust の設計では、LLVM はやや手抜きな面がありますが、選択は適切です。Go のように独自で機械コード生成プロセスを維持するのとは異なり、LLVM の大規模なエコシステムに成熟した最適化を任せるのは非常に有益です。Rust のコンパイルフローは大まかに次のようになります。ソースコードはまず字句解析と構文解析を行い、AST を生成します。その後、マクロ展開と HIR の降格でフラット化されます。続いて通常の型チェックと少量の借用チェックが行われ、最終的に MIR が生成されます。ここまではすべて Rust コンパイラのフロントエンドが担当します。MIR 以降の処理は LLVM に委ねられ、例えば MIR を LLVM IR に変換し、LLVM が O0 から O3 までの最適化パスを実行します。最後にターゲットプラットフォーム向けの機械コードが生成され、リンカがそれを結合して実行可能な BIN ファイルにします。
では一体どこに問題があるのでしょうか。答えは実はとてもシンプルです。LLVMは非常に重厚なインフラストラクチャであり、本番環境において重厚であることは正しく、コンパイル時間を削って実行時間を稼ぐというのは素晴らしい哲学です。LLVMは上位のRustをはじめ、C、C++、Swiftといった数多くの言語をブリッジしています。これは、その最適化パイプラインが「最適なマシンコードの生成」のために設計されているのであり、「コンパイルを迅速に完了させる」ために設計されているわけではないことを意味します。releaseモードで数十もの最適化passを一通り実行するのは極めて時間がかかりますし、debugモードのO0であっても、LLVMはIR生成からマシンコード生成に至るプロセス全体を通過しなければならず、このプロセス自体が決して軽量ではありません。さらに、Rustのジェネリクス単相化によってコンパイル時に大量のコードが展開されるため、LLVMに渡されるIRの量はソースコードの見かけよりも遥かに大きくなり、LLVMが処理すべき作業量も自ずと膨れ上がるのです。
Cranelift
本質的に、コンパイルが遅い時間の大部分は Rust のフロントエンドではなく、LLVM という「重量級バックエンド」に費やされています。そこで問題です:これを取り除くことは可能か? 答えは「はい、にゃん」。そのために作られたバックエンドが Cranelift です。もとは Cretonne と呼ばれ、2016 年に開始され、Bytecode Alliance によって開発されました。当初は Wasmtime パッケージ用のコード生成バックエンドとして設計され、後に Rust 公式に採用され、オプションのコードジェンバックエンドとなりました。

では、Cranelift は一体どこで速くあるべきなのでしょうか? 最高のフェーズはまさに開発時です。この段階では、実行効率が極限まで最適化された完璧に近いマシン語などおそらく必要なく、単に素早いフィードバックが得られれば十分かもしれません。このそこまで出来の良くないマシン語が、LLVM の正当な計算によるマシン語と意味的に等価でありさえすればいいのです。初期の Cranelift は多くの Edge Case が存在したため実際にはそれができませんでした。今でもまだ残ってはいるもののかなり改善されており、現在 Cranelift で動くのに LLVM で壊れる確率は、Rust を書いていて rustc ICE を引き起こす確率とほぼ同等です。LLVM は極限まで最適化されたマシン語を生成するために、ループ展開、ベクトル化、定数伝播、死にコード消去といった数十もの最適化 pass を走らせます…… かなり多くの処理を一層一層じっくり磨き上げるのです。一方で Cranelift の設計思想は全く異なり、これらの最適化ステップを大幅に削減して、最も基本的なレジスタ割り当てと命令選択だけを行い、反復することなく単一の線形スキャンでコード生成を完結させます。同時にその IR 設計もより軽量で、上位 IR からマシン語への高速な翻訳のために特別に設計されており、数十年分の汎用化という重荷を背負った LLVM IR とは異なります。
さて、メリットを話し終えたところで、では代償は何でしょうか? まず目につくのは、Cranelift が生成するコードの実行時パフォーマンスが LLVM よりも劣り、シナリオにもよりますがおよそ 10%〜30% 遅くなる点です。ですが、割り切って考えてみれば、開発段階ではそんなことはどうでもいいと気づくでしょう。何をそんなに急ぐ必要があるのでしょうか。ベンチマークを取っているのか、それとも CI release なのですか? こっちはただ cargo build を速く終わらせて、ロジックが動くかどうかを確認したいだけなのです。それに賭けてもいいですが、書いたプログラムの大半が時間の 80% 以上も計算リソースを食いつぶすなんてことはまずありません。何かしらのビジネスリクエストが来て動くものですが、開発中にどれほどの負荷がかかるというのでしょう? CPU なんて終始サボっているようなものです。それよりも結果を見てロジックを検証したいのです。コンパイル時間と実行効率のトレードオフという勘定は、開発サイクルにおいてはむしろ逆算すべきではないでしょうか。
コード生成ユニット
さらに codegen-units について説明します。これは実際には、コンパイラがクレートをバックエンドが並列に処理できる最小単位にいくつに分割するかを制御するパラメータです。デフォルトでは、debug モードでは 256 個、release モードでは 16 個です。数値が大きいほど並列度が上がり、コンパイルが速くなります――複数の CPU コアを同時に利用してバックエンドのコード生成を行えるからです。ただし、代償として最適化効果が低下します。各単位は独立して最適化されるため、LLVM(または Cranelift)が見るコンテキストが小さくなり、単位間のインライン化や最適化の機会が減少します。
しかし実際の Release では、通常 codegen-units = 1 のような極端なオプションを使用すべきです。そうしなければ、生成物の最適化を最大限に引き出すことはできません。結局のところ Rust を使っているわけですから、コンパイル時間を犠牲にして実行時性能を得るのは当然のことです。
最適化レベル
もう一つ見落とされがちですが調整できる設定項目が opt-level です。デフォルトでは "3" のはずですが、Release では "z" に設定するのがより一般的です。コードを変更せずに得られる無料の成果物サイズ最適化ですから、使わない手はありません。しかし、開発モードではここを "0" に設定し、全く最適化を行わないことで最速の速度を得るべきです。
さらに、Cargo.toml という小技があります。実は依存関係と自分のコードに異なる最適化レベルを設定できます。
[profile.dev.package."*"]
opt-level = 3これにより、サードパーティの依存関係を O3 でビルドできるようになります。増えるのは最初のクリーンビルドの時間だけで、それ以降は増分ビルドになります。外部依存関係は通常それほど頻繁に更新されないため、依存関係には O3 を使い、自身のビジネスロジックには O0 を使うことで、通常の開発において速度とサイズのバランスが取れた良好な dev モードの体験が得られます。ただし、Release モードにおいては、迷わず O3 または Z を最大まで設定することをお勧めします。
リンクタイム最適化
LTO(Link‑Time Optimization)は、メモリとコンパイル性能を大量に消費するもう一つの機能で、ほとんどのプロジェクトが Release ビルド時に有効にすることを推奨しています。通常のコンパイルでは、各 crate が独立して最適化され、コンパイラのバックエンドは crate 間の呼び出し関係を把握できないため、クロス crate のインライン化やデッドコード除去ができません。このようなケースは非常に多く、LTO はリンク時にすべての crate の IR を取得してこの境界を打ち破り、グローバルな最適化を行います。ただし、profile に無造作に lto = true を付けるのはやめてください。これをすると Rust のコンパイルが遅くなりすぎて人生に疑問を抱くほどです。release profile を使用する際にだけ付けるようにしましょう。
デバッグシンボルを削除
他のものと同様、Rust の debuginfo や symbols はここに存在しています。release モードで strip を有効にすると、サイズをかなり削減できます(通常は 50MB vs 5MB という途方もない違いです)。symbols を取り除くことは安全性の面でも有利です。結局のところ、フロントエンドの source code が漏洩する原因の多くは、map の中からうっかり npm に push されてしまうことなのですから :(
なので dev profile では特に言うことはありません、debuginfo を strip することはできませんよね、そうすると gdb / lldb を通常通りデバッグできず、backtrace に意味のある関数名も表示されません。ただし、ArchLinux でパッケージ化する際にはデフォルトで strip が行われますが、ここでは私たちがすでに strip してしまっているため、少し摩擦があります。その結果、後でエラーが出る可能性があります。AUR を作成する際にはこのステップを明示的にスキップすることに注意してください。
panic! の放棄
通常のビジネスロジックにおいて、panic は存在するべきではありません。通常のビジネスコードが runtime でエラーに遭遇した際、耐障害性が設計された Err であれば安全に返されるべきであり、強引に panic させるべきではありません。React ErrorBoundary と同様に、通常の異常として処理されるべきです。panic は、コードが絶対にあり得ない状態に陥り、復旧不可能であると確信したときにのみトリガーされるべきです。私個人の哲学としては panic を放棄することですが、なぜでしょうか?実際にはビジネスコードは 3 層のテストとして捉えることができるからです。第 1 層は happy path、第 2 層はエラーパス(無限の可能性の中のどれか 1 つ)、第 3 層になって初めて fuzz や衝突テストによって網羅された Edge Case となります。工学的に言えば、正常パスは 100% のテストが可能であり、エラーパスは 1 つのカテゴリ内の n 種類のバリエーションから 1 つを選べば十分です。衝突テストは純粋に時間の問題であり、ほとんどの場合はかける価値がありません。ユーザー規模が実際に拡大したとき、実施すべきタイミングは自然とわかるはずです。通常、私は一切行いません。
良いコードに対しては、1つの正常系テストと少なくとも1つのエラー系テストをカバーすべきです。そのエラー系テストは、Err が返されるケースをテストできれば十分で、フォールバックロジックを書き、自然に thiserror 列挙あるいは anyhow でインターセプトしてログを平坦化できます。要するに、コードはこれらの状況を処理する仕組みを書かせることになります。この層まで実装できていれば、panic はあなたにとって不要です—「理論上は不可能」と解釈できますが、それも理論上の話です。実際の世界では OS エラー、メモリエラー、宇宙線による単一ビット反転、奇妙なオーバーフローなどが存在します… すべてを網羅することは不可能です。ではなぜ panic を避けるのか? それらの状況は大半がコードの問題ではなく、90% はあなたのコードが原因ではありません。panic の本質は、発生時に Rust が呼び出しスタックを遡り、各フレームのデストラクタ(drop)を順に呼び出してリソースを解放することです。このプロセスにはコンパイラが余分な unwind テーブルを生成する必要があり、ビジネスロジックのエラー箇所を特定する手助けになります。エラーがほとんど自分のものではないなら、その情報に価値はありませんし、バイナリサイズも増え、リンク作業も増えるのでオフにするのが正解です。コードのテストが十分で自信があるなら、panic = abort を設定すると良いでしょう。
実際のベンチマーク
そんなに話すのは無駄です。実際にこれらの組み合わせから得られる向上を見てみましょう。ここでは、数か月前に作った小さな玩具プロジェクトを参考にします。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Language Files Lines Code Comments Blanks
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CSS 5 152 113 16 23
Go 77 14556 12409 584 1563
JavaScript 23 2506 1999 232 275
JSON 93 1780 1780 0 0
Just 1 408 275 71 62
Makefile 1 15 8 3 4
Shell 16 1440 1075 166 199
SVG 1 9 9 0 0
TOML 20 502 419 3 80
TSX 75 3257 2718 132 407
TypeScript 390 36643 30350 1627 4666
─────────────────────────────────────────────────────────────────────────────────
HTML 2 41 32 7 2
|- CSS 1 37 37 0 0
(Total) 78 69 7 2
─────────────────────────────────────────────────────────────────────────────────
Markdown 95 5344 0 3578 1766
|- BASH 3 8 8 0 0
|- Go 2 21 21 0 0
|- HTML 1 25 15 10 0
|- JSON 6 317 317 0 0
|- Rust 1 10 9 0 1
|- TSX 1 11 9 0 2
(Total) 5736 379 3588 1769
─────────────────────────────────────────────────────────────────────────────────
Rust 181 31654 26951 1032 3671
|- Markdown 86 581 0 564 17
(Total) 32235 26951 1596 3688
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total 980 99317 78554 8025 12738
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━このプロジェクトの Rust 部分は、依存関係のサイズを除き、約 32K SCoL Rust です。
このリポジトリは実際には多くのサブプロジェクトを持つ monorepo ですが、その中から 2 つの典型的な例を見つけることができます。まず Skeleton パッケージを見ると、その特徴は明らかです。コード量が多く、依存関係が極めて少ないため、プロジェクト全体の codegen の比率が最も高く、Cranelift が活躍するのに非常に適しています。また、このパッケージは「クリーン」でもあり、クリーンとは純粋な Safe Rust であることを指します。
一方で、以下の CLI パッケージはあまり適切ではありません。依存関係が多いこと自体は問題ではなく、理論上はこの方が効率性を発揮できますが、ここには古典的な二者択一が存在します。左上を見ると ring という依存関係がオプショナルとしてマークされているのが分かります。それはプロジェクトが元々 aws-lc-rs を使用していたためで、これらはどちらも Rust における暗号化アルゴリズムの Backend です。ではなぜオプショナルにする必要があるのかというと、ring は純粋な Rust で書かれているのに対し、もう一方は Asm アセンブリの FFI 経由で導入されており、x86 や arm64 などの主要な cpu アーキテクチャ向けにアセンブリ加速が施されているからです。しかし、これこそが裏目に出ており、Cranelift の魔法は純粋な Rust に限定されているため、Unsafe Code や FFI C、ASM などを導入した途端、実際には Cranelift の互換性が極めて悪くなってしまうのです。
しかし、これは解けないわけではなく、実際には cfg でバックエンドを選択し、Cranelift モードでは ring をコンパイラに渡すだけで済みます。上述の通りに dev と release のプロファイルを設定したら、CLI のコンパイル比較を簡単に実行できます。このケースでは、開発ビルドはリリースビルドより少なくとも 3 倍速く、しかもコールドコンパイルでインクリメンタルがない状態です;さらに O0 を使用し LTO を無効にすれば、dev プロファイルは今後のインクリメンタル更新でリリースより何十倍も速くなるはずです。LTO などの魔法は実際には各 crate の境界を平坦化することで実現されており、すべてが一体になるとコードの一箇所を変更しただけで全体が再コンパイルされ、正しくインクリメンタル更新できなくなります。
![これは「~/C/P/seam」というタイトルのターミナルウィンドウのスクリーンショットで、プロジェクト「seam」の `cargo build --release` 実行の最後の部分を示しています。スクロールには、walkdir、rand、tungstenite、sha2、toml、notify、rquickjs、clap など、順にコンパイルされる多数の依存クレートがリストされ、その後に seam-codegen、seam-skeleton、seam-injector-wasm、seam-engine-wasm、seam-server、seam-server-axum、seam-cli といったプロジェクト固有のワークスペースメンバーや、demo-server-rust、github-dashboard-axum、markdown-demo-rust、i18n-demo-axum といった例示的バックエンドが続きます。最終行は「Finished `release` profile [optimized] target(s) in 1m 05s」と成功を示し、続いてアイドル状態のシェルプロンプト `canmi@xyy ~/C/P/seam (main)>` が表示されています。これは、WASM と Axum サーバーコンポーネントを含むマルチクレート Rust モノレポのクリーンで完全なリリースビルドの証拠です。](https://cdn.ffoni.com/image/e8b530aa153a78f1cdea3d2980f80ecd.jpeg)
![ターミナルのスクリーンショットは、Rust ワークスペース「seam」の Cargo ビルドログの末尾を示し、タイトルは `~/C/P/seam`、シェルはユーザー `canmi` が `main` ブランチで実行しています。依存クレートが順にコンパイルされます(console、reqwest、clap、tokio-tungstenite、notify、indicatif など)。続いてプロジェクト独自のクレート—seam-codegen、seam-server-axum、seam-skeleton、seam-injector-wasm、seam-engine-wasm、seam-cli(すべてバージョン 0.5.38)—と、いくつかの例バックエンド(github-dashboard-axum、i18n-demo-axum、markdown-demo-rust、demo-server-rust)がコンパイルされます。ログは `Finished \`dev\` profile [unoptimized + debuginfo] target(s) in 19.23s` で終了し、例示アプリケーションを含むワークスペース全体のデバッグビルドが成功したことを示しています。](https://cdn.ffoni.com/image/71aa2631cd4c616d2ecca6e510cf06f4.jpeg)
総合的な結果を見ると、この2つは実際そこまで大差なく、劇的な変化とまではいかないものの、間違いなく体感できる違いがあります。主な理由は Apple Silicon があまりにも強力だからです。Mシリーズのチップはシングルコア性能とメモリ帯域幅が異常なほど高く、コンパイルのような計算重度+I/O重度のタスクがちょうどこれらの強みを享受できています。通常 Linux で走らせると差はさらに広がるはずですが、MacBook に Asahi Linux などを導入して動かすなら間違いなく Linux の勝ちでしょう。
古いリンカ
Linux では、Rust はデフォルトで GNU ld (bfd) を使用します。そう、あの最も古く最も遅い奴です。シングルスレッドであり、シンボル解決と再配置をシリアルにスキャンして処理するため、プロジェクトが大きくなると明らかに足を引っ張るようになります。Linux では @Rui Ueyama 氏が作成した mold を試してみるとよいでしょう。設定は非常に簡単で、.cargo/config.toml に 1 行追加するだけです。
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
向上した効果はかなり顕著ですが、残念ながら macOS 上には sold という商用プロジェクトしかありません。しかし嬉しいことに、macOS 上の lld や Apple 付属の ld64 自体がすでに遅くはありません。特に Xcode 15 以降の新しいリンカーは、速度向上が超顕著です。ただ、この代物の唯一の難点は、macOS の無人 CI 環境や更新後に毎回 sudo Accept で利用規約に同意する必要があり、そのせいで私の CI 定期タスクが何度か落ちてしまったことです。
MUSL
私自身は MUSL ファンで GNU glibc が大嫌いですが、開発にはやはり x86_64-unknown-linux-gnu の使用をおすすめします。なぜなら、速いからです。MUSL の最大の売りは、完全な静的リンクのバイナリを生成できることです。システム上の動的ライブラリに一切依存せず、ビルドしたものを任意の Linux マシンにコピーするだけで動作するため、コンテナや組み込みに最適です。しかし、遅いのもまた静的リンクが原因です。リンカーがすべてをパッケージングする必要があるため、リンク段階の作業量は動的リンクよりもかなり大きくなります。
しかし朗報として、macOS を使っているなら全く心配する必要はありません。aarch64-apple-darwin が現在唯一の選択肢となります。macOS は設計上、完全な静的リンクをサポートしていませんが、コンパイル速度の観点からはむしろ好都合です。そのため、dev profile では Linux で gnulibc、macOS で libSystem を使い、release profile では macOS と Linux の両方で musl を使用できます。macOS のデフォルトの linker と ar の選択の問題があるため、唯一注意すべき点として .cargo/config.toml の設定が必要になるかもしれないことです。
[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"
ar = "x86_64-linux-musl-ar"ちなみに、zig は絶対に使わないでください。nightly Rust の zbuild は現在大きな問題があります。クロスコンパイルが必要で、ホストが Linux x86 の場合は、cross を推奨します。Docker を使ってビルドすれば環境がすべて揃い、とても便利です。glibc が好きならそれも問題ありません。glibc は下位互換しか保証しないため、一般的には Debian のメジャーリリース(例: Debian‑2)の glibc バージョンでコンパイルします。新しすぎる glibc は多くのマシンで動かなくなるためです。これは新機能を使ったからではなく、単にバージョン文字列を判定して苛立たせるためです。ホストが mac の場合は rustup のネイティブ版を使用してください target + cargo build --release 。
UPX は悪いものです
もう一つ議論する価値のあるポイントは UPX の使用についてです。musl が好きだと言いながら同時に小さなサイズを追求するのは、実際かなり矛盾していて理にかなわないように聞こえるのは分かっていますが…
ooooo ooo ooooooooo. ooooooo ooooo
`888' `8' `888 `Y88. `8888 d8'
888 8 888 .d88' Y888..8P
888 8 888ooo88P' `8888'
888 8 888 .8PY888.
`88. .8' 888 d8' `888b
`YbodP' o888o o888o o88888o
The Ultimate Packer for eXecutables
Copyright (c) 1996-2026 Markus Oberhumer, Laszlo Molnar & John Reiser
https://upx.github.ioしかし実際には、UPX の存在によってこの問題は解決されます。UPX の原理は、BIN を圧縮した後に解凍 stub を被せ、実行時にまずメモリ内で解凍してから実行するというもので、これによりバイナリサイズを元の 30%〜50% 程度に圧縮できるため、サイズの優位性が非常に顕著になります。しかし、まさにそのために UPX + GNU glibc の動的リンクには大きな問題が生じます。glibc 動的リンクのバイナリには特殊な section や動的ロード機構が存在し、UPX 圧縮によってこれらが破壊され、実行時に動的ライブラリが見つからなくなる可能性があります segment fault。一方で musl を見ると、これは UPX に非常に適しています。もともと完全スタティックで内部も安定しているため、UPX 後の解凍と圧縮が非常にクリーンです。そのため、musl の release CI に UPX のステップを 1 つ追加するのは非常にポイントが高いと言えます。
また、UPX も乱用すべきではありません。UPX は長期間実行するタスク(長タスク)か低頻度タスクにのみ適しています。長タスクに適合するのは、UPX が起動時にメモリ上で一度解凍を行う必要があり、このプロセスは全自動であるものの多少のコールドスタート時間を要するためです。この影響は僅かですが、高頻度で呼び出されるパスにおいては非常に致命的となります。そのため、長タスクを一度起動してバックグラウンドで常駐させるような用途には非常に適しています。しかし、Docker イメージのような長期サービスのパッケージに思考停止で UPX を詰め込むことはおすすめしません。なぜなら、これは実質的にメモリとディスク容量をトレードオフする行為であり、大損だからです。さらに、コンテナ Layer の圧縮は実質的に UPX と同じ役割を果たし、配信時のダウンロードサイズを削減してくれます。むしろ推奨される環境は特定の CLI 用途です。膨大なディスク容量を節約できる上、起動時の解凍時間はほとんど体感できません。しかも musl は本質的に CLI 開発に向いています。さもなければ AUR ヘルパーを名指しで批判することになってしまいます。Go で書かれたあのツールは静的リンクがされておらず、以前 ArchLinux 上で yay が壊れて動的ライブラリが見つからなくなる事態に遭遇しました。それ自体がシステムパッケージマネージャーであるため自力での解決が不可能となり、最終的に LiveISO で起動して chroot して復旧するしかありませんでした。
要約
これらの便利な設定は dev と release プロファイルをすばやく区別するのに役立ちますが、プロジェクトのコードが増えるにつれてその効果はますます明らかになります。しかし、それでも Rust Stable LLVM ビルドを実行する CI を追加し、できればすべてのテストも一緒に走らせることをお勧めします。これによりローカルで nightly Rust に切り替える必要がなくなり、エッジケースにも悩まされず、プッシュ時に問題があれば CI がブロックしてくれます。現在、これが最も快適な DX 方式です。今回はここまでです。また時間があるときに続きます。