fix(build): verify native release binaries are portable by ryoppippi · Pull Request #1256 · ccusage/ccusage · GitHub
Skip to content

fix(build): verify native release binaries are portable#1256

Merged
ryoppippi merged 4 commits into
mainfrom
fix/verify-native-binary-portability
Jun 10, 2026
Merged

fix(build): verify native release binaries are portable#1256
ryoppippi merged 4 commits into
mainfrom
fix/verify-native-binary-portability

Conversation

@ryoppippi

@ryoppippi ryoppippi commented Jun 10, 2026

Copy link
Copy Markdown
Member

Summary

v20.0.10 shipped a macOS binary that was dynamically linked against /nix/store/.../libiconv.2.dylib, which crashed for every user without Nix with a missing dynamic library error. #1249 fixed the linkage with install_name_tool, but nothing guarded against the same class of regression. This PR adds verification at three layers so a non-portable binary can no longer ship silently.

What Changed

  • package.nix: the darwin build now fails in postInstall if any linked dylib lives outside /usr/lib or /System/Library, printing the offending paths. install_name_tool -change is a silent no-op when the old path does not match, so the rewrite alone is not a guarantee. Runs on every nix build .#ccusage and via nix flake check (which builds the package as a check).
  • nix/static-package.nix: ccusage-static now fails in postInstall if the binary has a PT_INTERP program header, i.e. it is not actually static. This moves the primary gate into the build itself, complementing the existing ldd step in the CI action.
  • apps/ccusage/scripts/ensure-native-binary.ts: staged package binaries are only accepted if they are portable (Linux: fully static via ldd; macOS: only system dylibs via otool -L; Windows: skipped). A version-matched but non-portable binary throws instead of being silently accepted.

Testing

  • darwin positive: nix build .#ccusage passes and otool -L shows only /usr/lib dylibs.
  • darwin negative: temporarily removing the install_name_tool line makes the build fail with the offending /nix/store path in the log.
  • ensure-native-binary.ts verified in both directions: accepts the fixed staged binary (exit 0) and throws on a version-matched Nix-linked binary.
  • Linux assertion cannot be built on this machine; nix eval .#packages.aarch64-linux.ccusage-static.drvPath confirms the derivation evaluates. The release CI build exercises it, with the existing ldd action step as a second net.
  • just fmt and just typecheck pass.

Related Issues

Follow-up hardening to #1249.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Add build, CI, and packaging checks to ensure native release binaries are portable. macOS links only system dylibs, Linux is fully static, and non‑portable staged binaries are rejected to prevent regressions like the Nix‑linked libiconv crash.

  • Bug Fixes
    • package.nix (darwin): fail if any dylib outside /usr/lib or /System/Library; print offenders.
    • nix/static-package.nix (linux-static): fail if PT_INTERP exists (not fully static).
    • apps/ccusage/scripts/ensure-native-binary.ts: only accept portable staged binaries (Linux static; macOS system dylibs).
    • .github/actions/build-macos-cargo-native-package: fail macOS x64 cargo build if non‑system dylibs; show offenders.

Written for commit fb1127f. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Chores
    • Added binary portability validation and build-time checks to ensure the native ccusage executable is compatible across platforms—verifies static linking on Linux and that macOS builds only use system-provided dynamic libraries. Build/CI now fail with clearer errors when validation fails.

ensure-native-binary.ts accepted any staged package binary whose
version matched, including binaries dynamically linked against
libraries that only exist on the build machine. A Nix-built macOS
binary linking /nix/store/.../libiconv.2.dylib shipped this way and
crashed for users without Nix with a missing dynamic library error.

Add an isPortableBinary() gate before accepting the staged binary:
Linux binaries must be fully static (ldd, mirroring the release CI
check), macOS binaries may only link system dylibs under /usr/lib or
/System/Library (otool -L), and Windows is not checked because MSVC
builds link only system DLLs by default. A version-matched but
non-portable binary now fails loudly instead of being silently
accepted or silently replaced by a local cargo build.
The darwin package relies on install_name_tool to rewrite the Nix
store libiconv reference to /usr/lib/libiconv.2.dylib (#1249), but
install_name_tool -change is a silent no-op when the old path does
not match, and nothing verified the final binary. A regression (for
example a new Nix-linked dylib) would ship unnoticed and crash for
users without /nix/store.

Assert in postInstall that every linked dylib lives under /usr/lib
or /System/Library, printing the offending entries otherwise. The
assertion runs on every nix build and via nix flake check, which
already builds the ccusage package as a check.

Verified both ways on darwin: the current build passes, and removing
the install_name_tool line makes the build fail with the offending
/nix/store path in the log.
ccusage-static targets musl specifically so the published Linux
binaries run without any dynamic libraries, but the derivation never
verified the result; only a separate CI action step did. Check in
postInstall that the binary has no PT_INTERP program header, which
would mean it requests a dynamic loader and fails on end-user
machines. READELF comes from the cross bintools wrapper, with a
plain readelf fallback.

This complements the existing ldd verification in
build-linux-native-package, moving the primary gate into nix build
itself so local builds and any future workflows are covered too.
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 10, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
ccusage-guide fb1127f Jun 10 2026, 09:36 PM

The darwin-x64 package is built with plain cargo on a GitHub macOS
runner, so the portability assertion added to the Nix derivation does
not cover it and no other gate inspects it before staging. Fail the
build action if otool -L reports any dylib outside /usr/lib or
/System/Library, mirroring the assertion in package.nix.

The check logic was exercised locally on macOS: a /usr/lib-linked
binary passes and a Nix-store-linked binary fails with the offending
path printed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/ccusage/scripts/ensure-native-binary.ts (1)

56-61: ⚡ Quick win

Consider using as const for the literal array.

The systemDylibPrefixes array is never modified and represents a fixed set of path prefixes. Applying as const makes it a readonly tuple, preventing accidental mutation and improving type inference.

As per coding guidelines, TypeScript files should use as const satisfies for typed literals.

♻️ Suggested refactor
-const systemDylibPrefixes = ['/usr/lib/', '/System/Library/'];
+const systemDylibPrefixes = ['/usr/lib/', '/System/Library/'] as const;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ccusage/scripts/ensure-native-binary.ts` around lines 56 - 61, The array
systemDylibPrefixes is a fixed literal and should be made an immutable typed
literal; change its declaration to use "as const" together with a "satisfies"
clause (e.g., make it a readonly tuple that satisfies the expected string-array
type) so it becomes readonly and improves type inference for references to
systemDylibPrefixes; update any consumers that expect a mutable string[] to
accept a readonly string[] or convert via Array.from(...) where necessary.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/ccusage/scripts/ensure-native-binary.ts`:
- Around line 56-61: The array systemDylibPrefixes is a fixed literal and should
be made an immutable typed literal; change its declaration to use "as const"
together with a "satisfies" clause (e.g., make it a readonly tuple that
satisfies the expected string-array type) so it becomes readonly and improves
type inference for references to systemDylibPrefixes; update any consumers that
expect a mutable string[] to accept a readonly string[] or convert via
Array.from(...) where necessary.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ba410b67-fdc9-489d-aa15-f3547d9b9ca2

📥 Commits

Reviewing files that changed from the base of the PR and between 0939b90 and df7e1f6.

📒 Files selected for processing (3)
  • apps/ccusage/scripts/ensure-native-binary.ts
  • nix/static-package.nix
  • package.nix

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="package.nix">

<violation number="1" location="package.nix:54">
P1: The dylib portability check is fail-open: if the inspection pipeline errors, the `if` condition just evaluates false and the build continues without enforcing portability.</violation>
</file>

<file name="nix/static-package.nix">

<violation number="1" location="nix/static-package.nix:62">
P1: The PT_INTERP verification is fail-open: an error running `readelf` can skip the check and allow the build to pass without validating static linkage.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread package.nix
# End-user machines have no /nix/store, so any dylib outside the macOS
# system paths would crash the published binary with a missing dynamic
# library error. grep prints the offending entries when it matches.
if otool -L $out/bin/ccusage | tail -n +2 | awk '{print $1}' | grep -Ev '^(/usr/lib/|/System/Library/)'; then

@cubic-dev-ai cubic-dev-ai Bot Jun 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The dylib portability check is fail-open: if the inspection pipeline errors, the if condition just evaluates false and the build continues without enforcing portability.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At package.nix, line 54:

<comment>The dylib portability check is fail-open: if the inspection pipeline errors, the `if` condition just evaluates false and the build continues without enforcing portability.</comment>

<file context>
@@ -48,6 +48,13 @@ craneLib.buildPackage (
+      # End-user machines have no /nix/store, so any dylib outside the macOS
+      # system paths would crash the published binary with a missing dynamic
+      # library error. grep prints the offending entries when it matches.
+      if otool -L $out/bin/ccusage | tail -n +2 | awk '{print $1}' | grep -Ev '^(/usr/lib/|/System/Library/)'; then
+        echo "error: ccusage links dylibs that do not exist on end-user machines" >&2
+        exit 1
</file context>
Fix with cubic

Comment thread nix/static-package.nix
# so it would not run on end-user machines without the build-time
# loader path. READELF is exported by the cross bintools wrapper.
postInstall = ''
if "''${READELF:-readelf}" -l $out/bin/ccusage | grep -q INTERP; then

@cubic-dev-ai cubic-dev-ai Bot Jun 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The PT_INTERP verification is fail-open: an error running readelf can skip the check and allow the build to pass without validating static linkage.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At nix/static-package.nix, line 62:

<comment>The PT_INTERP verification is fail-open: an error running `readelf` can skip the check and allow the build to pass without validating static linkage.</comment>

<file context>
@@ -55,6 +55,15 @@ in
+            # so it would not run on end-user machines without the build-time
+            # loader path. READELF is exported by the cross bintools wrapper.
+            postInstall = ''
+              if "''${READELF:-readelf}" -l $out/bin/ccusage | grep -q INTERP; then
+                echo "error: ccusage-static is not statically linked" >&2
+                exit 1
</file context>
Fix with cubic

@ryoppippi ryoppippi merged commit c408f64 into main Jun 10, 2026
22 of 23 checks passed
@ryoppippi ryoppippi deleted the fix/verify-native-binary-portability branch June 10, 2026 21:40
@github-actions

Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: df7e1f6e3d99
Base SHA: 0939b909d4f8

This compares the Rust PR release binary against the configured base package on the same CI runner.

Package runner startup

Execution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one bunx -p <url> ccusage --version run with an empty Bun install cache. Warm reuses that cache and reports the median of repeated runs.

Package SHA Execution setup Bunx temp cache Bunx warm median Warm samples
Base pkg.pr.new 0939b909d4f8 1.056s 816.9ms 51.7ms 3
PR pkg.pr.new df7e1f6 852.2ms 876.0ms 52.2ms 3

Cached bunx execution performance

Runs the same large fixture through bunx -p <pkg.pr.new URL> ccusage after the Bun install cache has already been populated by the startup measurement. This separates cached package-runner execution from first-fetch package materialization.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
Base package: 0939b909d4f8; PR package: df7e1f6. Both run through bunx -p <pkg.pr.new URL> ccusage using the warmed Bun install cache from package runner startup, measured by hyperfine with 0 warmups and 1 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
bunx -p <pkg> ccusage claude --offline --json 1.01 GiB 890.2ms 956.4ms 0.93x 728.50 MiB 726.50 MiB 1.00x 1.13 GiB/s 1.05 GiB/s
bunx -p <pkg> ccusage codex --offline --json 1.01 GiB 196.6ms 185.1ms 1.06x 90.75 MiB 91.75 MiB 1.01x 5.12 GiB/s 5.44 GiB/s

Package runtime diagnostics

Compares 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 /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
All rows run --offline --json, measured by hyperfine with 0 warmups and 1 runs. This isolates wrapper overhead from the installed native optional dependency and the workspace release binary built on the runner.

Command Runtime Input Median Throughput Samples
claude --offline --json Package wrapper 1.01 GiB 888.0ms 1.13 GiB/s 1
claude --offline --json Installed native binary 1.01 GiB 860.2ms 1.17 GiB/s 1
codex --offline --json Package wrapper 1.01 GiB 186.5ms 5.40 GiB/s 1
codex --offline --json Installed native binary 1.01 GiB 140.4ms 7.17 GiB/s 1

Committed fixture performance

Committed small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage.

Fixtures: Claude apps/ccusage/test/fixtures/claude (0.00 MiB, 2 files), Codex apps/ccusage/test/fixtures/codex (0.00 MiB, 1 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs rust/target/release/ccusage directly. Both run --offline --json, measured by hyperfine with 2 warmups and 7 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude daily --offline --json 0.00 MiB 46.8ms 7.7ms 6.05x 45.75 MiB 2.50 MiB 0.05x 0.03 MiB/s 0.20 MiB/s
claude session --offline --json 0.00 MiB 44.9ms 8.6ms 5.22x 43.75 MiB 2.50 MiB 0.06x 0.03 MiB/s 0.18 MiB/s
codex daily --offline --json 0.00 MiB 47.3ms 8.1ms 5.82x 43.50 MiB 2.50 MiB 0.06x 0.02 MiB/s 0.11 MiB/s
codex session --offline --json 0.00 MiB 50.9ms 7.8ms 6.50x 43.50 MiB 2.50 MiB 0.06x 0.02 MiB/s 0.11 MiB/s

Large real-world-shaped fixture performance

Generated 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 /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs rust/target/release/ccusage directly. Both run --offline --json, measured by hyperfine with 0 warmups and 1 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude --offline --json 1.01 GiB 890.5ms 888.6ms 1.00x 726.00 MiB 731.75 MiB 1.01x 1.13 GiB/s 1.13 GiB/s
codex --offline --json 1.01 GiB 185.2ms 144.9ms 1.28x 90.50 MiB 88.00 MiB 0.97x 5.44 GiB/s 6.95 GiB/s

Artifact size

Artifact Base PR Delta Ratio
packed ccusage-*.tgz 17.32 KiB 17.32 KiB +0.00 KiB 1.00x
installed native package binary 3417.74 KiB 3417.74 KiB +0.00 KiB 1.00x

Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees.

@github-actions

Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: df7e1f6e3d99
Base SHA: 0939b909d4f8

This compares the PR package against the configured base package on the same CI runner.

Package runner startup

Execution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one bunx -p <url> ccusage --version run with an empty Bun install cache. Warm reuses that cache and reports the median of repeated runs.

Package SHA Execution setup Bunx temp cache Bunx warm median Warm samples
Base pkg.pr.new 0939b909d4f8 785.3ms 815.3ms 49.4ms 3
PR pkg.pr.new df7e1f6 809.0ms 1.270s 50.7ms 3

Cached bunx execution performance

Runs the same large fixture through bunx -p <pkg.pr.new URL> ccusage after the Bun install cache has already been populated by the startup measurement. This separates cached package-runner execution from first-fetch package materialization.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
Base package: 0939b909d4f8; PR package: df7e1f6. Both run through bunx -p <pkg.pr.new URL> ccusage using the warmed Bun install cache from package runner startup, measured by hyperfine with 0 warmups and 1 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
bunx -p <pkg> ccusage claude --offline --json 1.01 GiB 844.0ms 893.4ms 0.94x 737.75 MiB 731.75 MiB 0.99x 1.19 GiB/s 1.13 GiB/s
bunx -p <pkg> ccusage codex --offline --json 1.01 GiB 217.5ms 196.7ms 1.11x 92.75 MiB 92.25 MiB 0.99x 4.63 GiB/s 5.12 GiB/s

Package runtime diagnostics

Compares 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 /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
All rows run --offline --json, measured by hyperfine with 0 warmups and 1 runs. This isolates wrapper overhead from the installed native optional dependency and the workspace release binary built on the runner.

Command Runtime Input Median Throughput Samples
claude --offline --json Package wrapper 1.01 GiB 979.3ms 1.03 GiB/s 1
claude --offline --json Installed native binary 1.01 GiB 926.8ms 1.09 GiB/s 1
codex --offline --json Package wrapper 1.01 GiB 180.8ms 5.57 GiB/s 1
codex --offline --json Installed native binary 1.01 GiB 140.6ms 7.16 GiB/s 1

Committed fixture performance

Committed small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage.

Fixtures: Claude apps/ccusage/test/fixtures/claude (0.00 MiB, 2 files), Codex apps/ccusage/test/fixtures/codex (0.00 MiB, 1 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published ccusage package from pkg.pr.new, installed before measurement. Both run --offline --json, measured by hyperfine with 2 warmups and 7 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude daily --offline --json 0.00 MiB 43.7ms 40.8ms 1.07x 45.75 MiB 45.75 MiB 1.00x 0.04 MiB/s 0.04 MiB/s
claude session --offline --json 0.00 MiB 41.2ms 40.7ms 1.01x 45.50 MiB 45.75 MiB 1.01x 0.04 MiB/s 0.04 MiB/s
codex daily --offline --json 0.00 MiB 44.3ms 40.2ms 1.10x 45.75 MiB 45.75 MiB 1.00x 0.02 MiB/s 0.02 MiB/s
codex session --offline --json 0.00 MiB 40.7ms 41.3ms 0.98x 45.75 MiB 45.75 MiB 1.00x 0.02 MiB/s 0.02 MiB/s

Large real-world-shaped fixture performance

Generated 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 /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published ccusage package from pkg.pr.new, installed before measurement. Both run --offline --json, measured by hyperfine with 0 warmups and 1 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude --offline --json 1.01 GiB 899.9ms 812.4ms 1.11x 748.25 MiB 742.00 MiB 0.99x 1.12 GiB/s 1.24 GiB/s
codex --offline --json 1.01 GiB 171.4ms 168.0ms 1.02x 88.00 MiB 90.25 MiB 1.03x 5.87 GiB/s 5.99 GiB/s

Artifact size

Artifact Base PR Delta Ratio
packed ccusage-*.tgz 17.32 KiB 17.32 KiB +0.00 KiB 1.00x
installed native package binary 3417.74 KiB 3417.74 KiB +0.00 KiB 1.00x

Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees.

@github-actions

Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: fb1127f162af
Base SHA: 0939b909d4f8

This compares the PR package against the configured base package on the same CI runner.

Package runner startup

Execution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one bunx -p <url> ccusage --version run with an empty Bun install cache. Warm reuses that cache and reports the median of repeated runs.

Package SHA Execution setup Bunx temp cache Bunx warm median Warm samples
Base pkg.pr.new 0939b909d4f8 672.9ms 871.7ms 59.3ms 3
PR pkg.pr.new fb1127f 963.0ms 792.1ms 47.3ms 3

Cached bunx execution performance

Runs the same large fixture through bunx -p <pkg.pr.new URL> ccusage after the Bun install cache has already been populated by the startup measurement. This separates cached package-runner execution from first-fetch package materialization.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
Base package: 0939b909d4f8; PR package: fb1127f. Both run through bunx -p <pkg.pr.new URL> ccusage using the warmed Bun install cache from package runner startup, measured by hyperfine with 0 warmups and 1 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
bunx -p <pkg> ccusage claude --offline --json 1.01 GiB 888.7ms 935.1ms 0.95x 740.25 MiB 723.50 MiB 0.98x 1.13 GiB/s 1.08 GiB/s
bunx -p <pkg> ccusage codex --offline --json 1.01 GiB 184.0ms 188.0ms 0.98x 89.50 MiB 88.25 MiB 0.99x 5.47 GiB/s 5.35 GiB/s

Package runtime diagnostics

Compares 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 /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
All rows run --offline --json, measured by hyperfine with 0 warmups and 1 runs. This isolates wrapper overhead from the installed native optional dependency and the workspace release binary built on the runner.

Command Runtime Input Median Throughput Samples
claude --offline --json Package wrapper 1.01 GiB 969.1ms 1.04 GiB/s 1
claude --offline --json Installed native binary 1.01 GiB 893.4ms 1.13 GiB/s 1
codex --offline --json Package wrapper 1.01 GiB 180.2ms 5.59 GiB/s 1
codex --offline --json Installed native binary 1.01 GiB 140.2ms 7.18 GiB/s 1

Committed fixture performance

Committed small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage.

Fixtures: Claude apps/ccusage/test/fixtures/claude (0.00 MiB, 2 files), Codex apps/ccusage/test/fixtures/codex (0.00 MiB, 1 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published ccusage package from pkg.pr.new, installed before measurement. Both run --offline --json, measured by hyperfine with 2 warmups and 7 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude daily --offline --json 0.00 MiB 40.5ms 43.8ms 0.92x 45.75 MiB 45.75 MiB 1.00x 0.04 MiB/s 0.04 MiB/s
claude session --offline --json 0.00 MiB 40.1ms 41.2ms 0.97x 45.75 MiB 45.75 MiB 1.00x 0.04 MiB/s 0.04 MiB/s
codex daily --offline --json 0.00 MiB 40.6ms 40.1ms 1.01x 45.50 MiB 46.00 MiB 1.01x 0.02 MiB/s 0.02 MiB/s
codex session --offline --json 0.00 MiB 40.4ms 43.6ms 0.93x 43.50 MiB 45.75 MiB 1.05x 0.02 MiB/s 0.02 MiB/s

Large real-world-shaped fixture performance

Generated 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 /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published ccusage package from pkg.pr.new, installed before measurement. Both run --offline --json, measured by hyperfine with 0 warmups and 1 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude --offline --json 1.01 GiB 996.6ms 875.7ms 1.14x 727.50 MiB 739.25 MiB 1.02x 1.01 GiB/s 1.15 GiB/s
codex --offline --json 1.01 GiB 172.3ms 177.2ms 0.97x 90.75 MiB 91.25 MiB 1.01x 5.84 GiB/s 5.68 GiB/s

Artifact size

Artifact Base PR Delta Ratio
packed ccusage-*.tgz 17.32 KiB 17.32 KiB +0.00 KiB 1.00x
installed native package binary 3417.74 KiB 3417.74 KiB +0.00 KiB 1.00x

Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees.

@github-actions

Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: fb1127f162af
Base SHA: 0939b909d4f8

This compares the Rust PR release binary against the configured base package on the same CI runner.

Package runner startup

Execution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one bunx -p <url> ccusage --version run with an empty Bun install cache. Warm reuses that cache and reports the median of repeated runs.

Package SHA Execution setup Bunx temp cache Bunx warm median Warm samples
Base pkg.pr.new 0939b909d4f8 700.8ms 966.4ms 56.4ms 3
PR pkg.pr.new fb1127f 922.1ms 856.8ms 53.5ms 3

Cached bunx execution performance

Runs the same large fixture through bunx -p <pkg.pr.new URL> ccusage after the Bun install cache has already been populated by the startup measurement. This separates cached package-runner execution from first-fetch package materialization.

Fixtures: Claude /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
Base package: 0939b909d4f8; PR package: fb1127f. Both run through bunx -p <pkg.pr.new URL> ccusage using the warmed Bun install cache from package runner startup, measured by hyperfine with 0 warmups and 1 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
bunx -p <pkg> ccusage claude --offline --json 1.01 GiB 935.5ms 934.1ms 1.00x 736.50 MiB 732.25 MiB 0.99x 1.08 GiB/s 1.08 GiB/s
bunx -p <pkg> ccusage codex --offline --json 1.01 GiB 188.4ms 211.7ms 0.89x 90.25 MiB 90.75 MiB 1.01x 5.34 GiB/s 4.75 GiB/s

Package runtime diagnostics

Compares 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 /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
All rows run --offline --json, measured by hyperfine with 0 warmups and 1 runs. This isolates wrapper overhead from the installed native optional dependency and the workspace release binary built on the runner.

Command Runtime Input Median Throughput Samples
claude --offline --json Package wrapper 1.01 GiB 897.2ms 1.12 GiB/s 1
claude --offline --json Installed native binary 1.01 GiB 885.2ms 1.14 GiB/s 1
codex --offline --json Package wrapper 1.01 GiB 183.7ms 5.48 GiB/s 1
codex --offline --json Installed native binary 1.01 GiB 135.3ms 7.44 GiB/s 1

Committed fixture performance

Committed small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage.

Fixtures: Claude apps/ccusage/test/fixtures/claude (0.00 MiB, 2 files), Codex apps/ccusage/test/fixtures/codex (0.00 MiB, 1 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs rust/target/release/ccusage directly. Both run --offline --json, measured by hyperfine with 2 warmups and 7 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude daily --offline --json 0.00 MiB 45.5ms 7.3ms 6.25x 45.75 MiB 2.50 MiB 0.05x 0.03 MiB/s 0.21 MiB/s
claude session --offline --json 0.00 MiB 41.2ms 7.2ms 5.72x 45.75 MiB 2.50 MiB 0.05x 0.04 MiB/s 0.21 MiB/s
codex daily --offline --json 0.00 MiB 39.7ms 6.3ms 6.29x 45.25 MiB 2.50 MiB 0.06x 0.02 MiB/s 0.14 MiB/s
codex session --offline --json 0.00 MiB 41.8ms 6.9ms 6.10x 45.75 MiB 2.50 MiB 0.05x 0.02 MiB/s 0.13 MiB/s

Large real-world-shaped fixture performance

Generated 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 /home/runner/_work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/_work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs rust/target/release/ccusage directly. Both run --offline --json, measured by hyperfine with 0 warmups and 1 runs.
Peak RSS is measured separately with /usr/bin/time using 1 runs. Lower RSS ratios are better.

Command Input Base median PR median PR vs base Base peak RSS PR peak RSS PR/base RSS Base throughput PR throughput
claude --offline --json 1.01 GiB 849.4ms 875.7ms 0.97x 727.00 MiB 735.50 MiB 1.01x 1.19 GiB/s 1.15 GiB/s
codex --offline --json 1.01 GiB 169.4ms 138.5ms 1.22x 90.25 MiB 89.75 MiB 0.99x 5.94 GiB/s 7.27 GiB/s

Artifact size

Artifact Base PR Delta Ratio
packed ccusage-*.tgz 17.32 KiB 17.32 KiB +0.00 KiB 1.00x
installed native package binary 3417.74 KiB 3417.74 KiB +0.00 KiB 1.00x

Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees.

ryoppippi added a commit that referenced this pull request Jun 11, 2026
* fix(nix): drop unused libiconv from the Darwin build

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.

* fix(nix): strip the unused libiconv dependency with dead_strip_dylibs

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

* fix(nix): fall back to libiconv install_name_tool rewrite when dead_strip_dylibs doesn't strip it

---------

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant