oven-sh/bun · error · Error

${nm} reported no text symbols — is ${bunProfile} stripped?

Error message

${nm} reported no text symbols — is ${bunProfile} stripped?

What it means

nm succeeded but the filter /^([0-9a-f]+) ([tT]) (\S+)$/ matched zero defined text symbols in the output, so the address→name map is empty. As the message says, the canonical cause is a stripped binary — nm lists no local/global text symbols to match.

Source

Thrown at scripts/orderfile/generate.ts:152

 * stripping — lld and ld take exactly what nm gave.
 */
function readSymbolTable(bunProfile: string): Map<number, string[]> {
  // Bare `nm` with no GNU-only long options: the regex below is the
  // defined-text-symbol filter, and nothing here depends on output order.
  const nm = process.env.NM || "nm";
  const r = runCommand([nm, bunProfile]);
  if (r.status !== 0) throw new Error(`${nm} failed on ${bunProfile}\n${r.stderr}`);

  const symbols = new Map<number, string[]>();
  for (const line of r.stdout.toString().split("\n")) {
    const m = /^([0-9a-f]+) ([tT]) (\S+)$/.exec(line);
    if (!m) continue;
    const address = parseInt(m[1]!, 16);
    const names = symbols.get(address);
    if (names) names.push(m[3]!);
    else symbols.set(address, [m[3]!]);
  }
  if (symbols.size === 0) throw new Error(`${nm} reported no text symbols — is ${bunProfile} stripped?`);
  return symbols;
}

/** Write function starts for functrace.c: u64 magic, version, count, addresses. */
function writeStarts(path: string, addresses: number[]): void {
  const buffer = new ArrayBuffer((STARTS_HEADER_WORDS + addresses.length) * 8);
  const words = new BigUint64Array(buffer);
  words[0] = STARTS_MAGIC;
  words[1] = 1n;
  words[2] = BigInt(addresses.length);
  for (let i = 0; i < addresses.length; i++) words[STARTS_HEADER_WORDS + i] = BigInt(addresses[i]!);
  writeFileSync(path, new Uint8Array(buffer));
}

/** Read a trace functrace.c wrote: first-entry addresses, slide already removed. */
function readTrace(path: string, name: string): number[] {
  const raw = readFileSync(path);
  if (raw.byteLength < TRACE_HEADER_WORDS * 8) throw new Error(`workload "${name}" wrote a truncated trace`);

View on GitHub (pinned to 8c5296ac45)

Solutions

  1. Build the unstripped profiling binary first: `bun run build:release` produces `bun-profile`
  2. Pass exeName explicitly if your build names the unstripped binary differently (e.g. assertions builds)
  3. Sanity-check with `nm build/bun-profile | grep -c ' [tT] '` — it must be non-zero before rerunning
  4. If using a custom NM, verify its output still looks like `0000000000001234 t _functionName`

Example fix

// before — pointing at the stripped artifact
generateOrderFile({ buildDir, exeName: "bun" });

// after — the unstripped symbol-bearing binary
generateOrderFile({ buildDir, exeName: "bun-profile" });
Defensive patterns

Strategy: validation

Validate before calling

import { spawnSync } from "node:child_process";

const r = spawnSync(process.env.NM || "nm", [bunProfile], { maxBuffer: 1 << 29 });
const textSymbols = r.stdout
  .toString()
  .split("\n")
  .filter((line) => /^[0-9a-f]+ [tT] \S+$/.test(line)).length;
if (textSymbols === 0) {
  throw new Error(`${bunProfile} has no text symbols (stripped?) — rebuild unstripped with bun run build:release`);
}

Prevention

When it happens

Trigger: Pointing exeName (or the default name) at the stripped release `bun` instead of the unstripped `bun-profile`; a release configuration that strips during copy; an nm variant that emits a different line format so every line fails the regex.

Common situations: Running generate against the wrong artifact in build output; build scripts changed to strip; switching nm implementations (some print symbol type in a different column).

Related errors


AI-assisted analysis of oven-sh/bun@8c5296ac45 (2026-08-16). Data as JSON: /api/errors/a72ebbde60e7971e. Report an issue: GitHub.