Repository navigation
fix(nix): strip the unused libiconv dependency on Darwin - #1292
Conversation
ccusage links no iconv symbols (verified: `nm -u` shows none on both arm64 and x64, no iconv crate in Cargo.lock), but libiconv was listed in the Darwin buildInputs, so the linker recorded /usr/lib/libiconv.2.dylib as an unused load-time dependency. When the Darwin package was built with Nix that path was the /nix/store one, which dyld can't resolve on non-Nix Macs — the crash behind #1251 (worked around in #1256 by rewriting the install name). Remove libiconv from buildInputs so the binary links only system dylibs and the install-name rewrite is no longer needed. The portability gate (otool, reject non-system dylibs) stays to catch any future stray /nix dependency.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughDrop the ChangesRemove libiconv dependency from macOS build
Sequence DiagramsequenceDiagram
participant ccusage as ccusage (binary)
participant otool as otool -L
participant installer as install_name_tool
participant validator as otool-validator
ccusage->>otool: list linked dylibs
otool->>installer: identify /nix/store/...-libiconv-... targets
installer->>ccusage: rewrite install-name -> /usr/lib/libiconv.2.dylib
ccusage->>validator: final otool -L check for non-system dylibs
🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
ccusage-guide | 6da7e0c | Commit Preview URL Branch Preview URL |
Jun 11 2026, 10:58 PM |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — Removes unused libiconv from package.nix's Darwin buildInputs and the associated install_name_tool rewrite, fixing the startup crash on non-Nix macOS (#1251). The otool gate is preserved to catch any future stray /nix/store dylibs.
- Drop
libiconvfrom Darwin build inputs — removed from function args andbuildInputs;nm -uconfirms no iconv symbols exist, so the linker was recording an unused load-time dependency. - Remove the
install_name_toolrewrite — no non-system dylibs remain, so the rewrite is dead code. - Update the
otoolcomment — explains why libiconv is gone while keeping the gate itself.
Big Pickle (free via Pullfrog for OSS) | 𝕏
|
Diagnosis: The Fix: Added a Task list (5/5 completed)
|
Removing libiconv from buildInputs was not enough: the nixpkgs Darwin stdenv injects -liconv regardless, so the arm64 binary still recorded the unused /nix/store libiconv dylib (the otool gate caught it). ccusage references no iconv symbols, so add -Wl,-dead_strip_dylibs to the Darwin link, which drops dylib load commands with no used symbols. The arm64 binary now links only /usr/lib/libSystem.B.dylib (verified), so the install-name rewrite stays gone. (Linux ships a fully static musl binary with no dynamic deps, so no equivalent is needed there.)
ccusage
@ccusage/ccusage-darwin-arm64
@ccusage/ccusage-darwin-x64
@ccusage/ccusage-linux-arm64
@ccusage/ccusage-linux-x64
@ccusage/ccusage-win32-x64
commit: |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — The prior 7afcec6 attempt to fix the Darwin crash by removing libiconv from buildInputs was incomplete: the nixpkgs Darwin stdenv still injects -liconv, leaving the unused dylib load command. The new commit adds -Wl,-dead_strip_dylibs to the Darwin link, which drops unreferenced dylib load commands — the proper root-cause fix.
- Add
-Wl,-dead_strip_dylibsto DarwinRUSTFLAGS— strips the unusedlibiconvdylib load command that the Darwin stdenv injects. The binary now links only/usr/lib/libSystem.B.dylibas verified by the author. - Reflow
RUSTFLAGSto multi-line expression — adds a comment referencing #1251 explaining why the flag is needed.
Big Pickle (free via Pullfrog for OSS) | 𝕏
…trip_dylibs doesn't strip it
ccusage performance comparisonPR SHA: This compares the PR package against the configured base package on the same CI runner. Package runner startupExecution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one
Cached bunx execution performanceRuns the same large fixture through Fixtures: Claude
Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |
ccusage performance comparisonPR SHA: This compares the Rust PR release binary against the configured base package on the same CI runner. Package runner startupExecution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one
Cached bunx execution performanceRuns the same large fixture through Fixtures: Claude
Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |
ccusage performance comparisonPR SHA: This compares the PR package against the configured base package on the same CI runner. Package runner startupExecution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one
Cached bunx execution performanceRuns the same large fixture through Fixtures: Claude
Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |
ccusage performance comparisonPR SHA: This compares the Rust PR release binary against the configured base package on the same CI runner. Package runner startupExecution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one
Cached bunx execution performanceRuns the same large fixture through Fixtures: Claude
Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |

Summary
ccusage references no iconv symbols (
nm -uon both arches, no iconv crate),yet the Darwin binary recorded
libiconv.2.dylibas an unused load-timedependency. Built with Nix that path was the
/nix/storeone, which dyldcan't resolve on non-Nix Macs — the crash behind #1251 (worked around in #1256
by rewriting the install name).
Root cause (corrected)
Removing libiconv from
buildInputsis not enough — the nixpkgs Darwinstdenv injects
-liconvregardless, so the unused dylib is still recorded.dyld loads every recorded dylib at launch whether or not its symbols are used,
so an unused dep with an unresolvable path aborts the process before
main.Fix
Add
-Wl,-dead_strip_dylibsto the Darwin link, which drops dylib load commandswith no referenced symbols. The unused libiconv is removed and the binary links
only
/usr/lib/libSystem.B.dylib(verified locally on arm64). The #1256install-name rewrite is no longer needed; the otool portability gate stays.
Linux ships a fully static musl binary (no dynamic deps), so no equivalent
(
--as-needed) is needed there.Validation
otool -L→ only/usr/lib/libSystem.B.dylib.runtime check.
Summary by CodeRabbit