Skip to content

build: make the tinycc option reach the Rust side - #39052

Open
robobun wants to merge 1 commit into
mainfrom
farm/a262ea7a/tinycc-option-reaches-rust
Open

build: make the tinycc option reach the Rust side#39052
robobun wants to merge 1 commit into
mainfrom
farm/a262ea7a/tinycc-option-reaches-rust

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • A build configured with --tinycc=off on linux, macOS or Windows fails to link: ld.lld: error: undefined symbol: tcc_new (and the other 13 tcc_* symbols), every reference coming from libbun_rust.a (<bun_tcc_sys::tcc::State>::new and the other wrappers). Log in the details block below.
  • The option only reaches the dependency graph. scripts/build/deps/tinycc.ts is enabled: cfg => cfg.tinycc, so libtcc leaves the link, but the Rust build is configured identically either way: scripts/build/buildOptionsRs.ts:69 emitted ENABLE_TINYCC as a hard-coded !cfg!(any(target_os = "android", target_os = "freebsd")) (the option's default from config.ts, not its value), and src/tcc_sys/tcc.rs:30,35 gated the tcc_* extern block and its stubs on a third copy of the same target list. The Zig build passed the option through as -Denable_tinycc=${cfg.tinycc}; the Rust port replaced that with these copies.
  • Had the link succeeded, ENABLE_TINYCC would still be true and cc() would call into a libtcc that is not there. The same applies to any change to the platform list in config.ts (for example enabling FreeBSD, feat(ffi): enable bun:ffi on FreeBSD #31528): it does not reach Rust unless both copies are edited too.

Fix

  • rust.ts (cargoBuildInvocation) passes --check-cfg=cfg(bun_tinycc) always and --cfg=bun_tinycc exactly when cfg.tinycc, the way socket_fault_injection is passed today; that option has the same shape (a C symbol and the Rust extern referencing it have to be present or absent together).
  • buildOptionsRs.ts emits ENABLE_TINYCC = cfg!(bun_tinycc); tcc_externs! picks the extern block under cfg(bun_tinycc) and the stubs under cfg(not(bun_tinycc)); Cargo.toml registers the cfg in unexpected_cfgs next to the other RUSTFLAGS cfgs; env.rs documents the constant.
  • Why this is the right shape: a #[cfg] on an extern block cannot read a constant, so a cfg is the only thing that can carry the option to the externs, and deriving ENABLE_TINYCC from that same cfg makes the runtime gate and the set of declared symbols agree by construction in every build, rather than through three lists kept in sync by hand. config.ts stays the one place that knows which platforms lack TinyCC.
  • Nothing CI builds changes: cfg.tinycc resolves to true on linux/darwin/windows (externs, ENABLE_TINYCC == true, as before) and false on android/freebsd (stubs, false, as before). The new --check-cfg flag changes RUSTFLAGS, so the first Rust build after this lands is a full one.
  • Bare cargo (cargo check, rust:check-all, clippy, miri) runs without rust.ts's rustflags and now compiles the stub arm on every target, where it previously compiled the extern arm on most of them. Both arms expand from the same macro input, and the extern arm is compiled by every bun bd and CI build. This is the existing convention for bun_asan / bun_debug / socket_fault_injection.
  • Verified:
    • test/internal/source-lints/build-rust.test.ts, new tinycc option block: resolves linux, windows and freebsd configs with and without the option, generates build_options.rs into a temp dir, and checks that the cfg it names is declared by cargoBuildInvocation()'s rustflags and set exactly when cfg.tinycc; that Cargo.toml registers it; and that tcc_externs! gates on that cfg with no target predicate, with no other fn tcc_* anywhere in src. The 8 new tests fail on main; the tcc.rs test also fails with only src/ reverted.
    • Debug build of main, then --tinycc=off: cargo is not re-run and the relink fails with 14 undefined tcc_* symbols. With this change: 21 deps, links, cc() throws bun:ffi cc() is not available in this build (TinyCC is disabled), dlopen and JSCallback still work, nm finds no tcc_* symbol. Default bun bd afterwards: tcc_* present, test/js/bun/ffi/cc.test.ts passes.
    • cargo check -p bun_tcc_sys and cargo clippy -p bun_tcc_sys are clean both without flags and with RUSTFLAGS="--cfg bun_tinycc" (the workspace denies warnings).
    • bunx tsc -p scripts/build/tsconfig.json: same 11 pre-existing errors before and after, none in the touched files.

Background

  • The tinycc option (Config.tinycc in scripts/build/config.ts, --tinycc=on|off on the command line) defaults to off on Android and FreeBSD, which the vendored oven-sh/tinycc cannot target. It decides whether the tinycc dependency (libtcc, compiled straight into bun's link) is built. bun:ffi uses TinyCC only for cc(); dlopen, JSCallback and friends are implemented in the engine.
  • build_options.rs is generated at configure time by buildOptionsRs.ts from the resolved Config and include!d by bun_core. bun_core::Environment::ENABLE_TINYCC comes from it; ffi_body.rs checks it at the top of cc() and throws before touching TinyCC.
  • tcc_externs! in src/tcc_sys/tcc.rs is the one place the tcc_* C functions are declared. The wrappers that call them are reachable from generated JS bindings, so rustc emits the references regardless of the runtime check; a build without libtcc therefore needs same-named stub definitions (which unreachable!()) in place of the extern block for the link to resolve.
  • rust.ts hands build options to Rust as --cfg=<name> in CARGO_ENCODED_RUSTFLAGS, with a matching --check-cfg so rustc knows the name. The unexpected_cfgs list in the root Cargo.toml declares the same names for cargo runs that do not go through rust.ts; those runs see every such cfg unset.

Related: #38913 does the same for the logs option and touches adjacent lines in rust.ts, buildOptionsRs.ts and Cargo.toml; whichever lands second has a trivial rebase. After this, #31528 (FreeBSD support) only needs the config.ts and dependency changes.

Link failure on main with --tinycc=off
$ bun scripts/build.ts            # main, debug build
$ bun scripts/build.ts --tinycc=off
21 deps, 123 codegen, 1211 objects in 932ms
[0/2] link bun-debug
ld.lld: error: undefined symbol: tcc_add_symbol
>>> referenced by tcc.rs:368 (src/tcc_sys/tcc.rs:368)
>>>               bun_tcc_sys-83bdef5b4688d405...rcgu.o:(<bun_tcc_sys::tcc::State>::add_symbol) in archive rust-target/x86_64-unknown-linux-gnu/debug/libbun_rust.a
ld.lld: error: undefined symbol: tcc_get_symbol
ld.lld: error: undefined symbol: tcc_add_library
ld.lld: error: undefined symbol: tcc_set_options
ld.lld: error: undefined symbol: tcc_define_symbol
ld.lld: error: undefined symbol: tcc_compile_string
ld.lld: error: undefined symbol: tcc_set_output_type
ld.lld: error: undefined symbol: tcc_add_library_path
ld.lld: error: undefined symbol: tcc_add_sysinclude_path
ld.lld: error: undefined symbol: tcc_new
ld.lld: error: undefined symbol: tcc_delete
ld.lld: error: undefined symbol: tcc_add_file
ld.lld: error: undefined symbol: tcc_relocate
ld.lld: error: undefined symbol: tcc_set_error_func
clang++: error: linker command failed with exit code 1

Configure-only evidence for the same thing: bun scripts/build.ts --profile=release --tinycc=off --build-dir=/tmp/x --configure-only produces a build.ninja with no libtcc edges, while /tmp/x/codegen/build_options.rs and the cargo edge's CARGO_ENCODED_RUSTFLAGS are byte-identical to a default configure.

With this change, the same --tinycc=off build links, and:

$ bun-debug -e 'import { cc, dlopen, JSCallback } from "bun:ffi"; ...'
cc threw: bun:ffi cc() is not available in this build (TinyCC is disabled)
JSCallback ptr ok: true
dlopen strlen: 5n
$ nm bun-debug | grep -c ' tcc_'
0

`--tinycc=off` (and the Android / FreeBSD defaults) only removed libtcc
from the dependency graph. build_options.rs still emitted ENABLE_TINYCC
as a hard-coded target predicate and bun_tcc_sys gated its tcc_* externs
on a copy of the same predicate, so a linux/macos/windows build with the
option off failed to link with undefined tcc_* symbols.

rust.ts now passes --check-cfg=cfg(bun_tinycc) always and --cfg=bun_tinycc
when cfg.tinycc, like socket_fault_injection. build_options.rs emits
ENABLE_TINYCC = cfg!(bun_tinycc), tcc_externs! switches between the extern
block and the stubs on the same cfg, and Cargo.toml registers the cfg for
bare cargo, which compiles the stubs.
@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 12:05 PM PT - Aug 15th, 2026

@robobun, your commit 4a5c794 has some failures in Build #98102 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 39052

That installs a local version of the PR into your bun-39052 executable, so you can run:

bun-39052 --bun

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Status

  • Reproduced on main (732491c): debug build, then bun scripts/build.ts --tinycc=off relinks without re-running cargo and fails with 14 ld.lld: error: undefined symbol: tcc_* errors, all from libbun_rust.a. Configure alone shows the cause: --tinycc=off removes the libtcc edges from build.ninja while build_options.rs and the cargo edge's rustflags stay byte-identical.
  • Fix: the option is passed to Rust as cfg(bun_tinycc) and ENABLE_TINYCC plus the tcc_* externs both key on it. The same --tinycc=off build now links and cc() throws "not available in this build"; the default build is unchanged.
  • Test: test/internal/source-lints/build-rust.test.ts (tinycc option block), fails on main, passes here.

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 29 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7b8d126c-2fd5-4aec-8700-ad17a4762c13

📥 Commits

Reviewing files that changed from the base of the PR and between 88a6398 and 4a5c794.

📒 Files selected for processing (6)
  • Cargo.toml
  • scripts/build/buildOptionsRs.ts
  • scripts/build/rust.ts
  • src/bun_core/env.rs
  • src/tcc_sys/tcc.rs
  • test/internal/source-lints/build-rust.test.ts

Comment @coderabbitai help to get the list of available commands.

@claude claude 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.

LGTM — mechanical build-plumbing fix that follows the existing socket_fault_injection / bun_asan / bun_debug pattern exactly.

What was reviewed:

  • Verified cfg.tinycc in config.ts defaults to !(android || freebsd), so CI-built binaries are unchanged (linux/darwin/windows still get externs, android/freebsd still get stubs).
  • Confirmed bun_tcc_sys/Cargo.toml has [lints] workspace = true, so the root unexpected_cfgs registration reaches it for bare cargo check.
  • Checked ffi_body.rs:993 — the ENABLE_TINYCC runtime gate is intact and now agrees with the extern/stub selection by construction.
  • No CODEOWNER-protected paths touched.
Extended reasoning...

Overview

This PR fixes --tinycc=off builds, which currently fail to link with 14 undefined tcc_* symbols because the option only reaches the C dependency graph (removes libtcc from the link) but not the Rust build (which still declares the externs). The fix threads cfg.tinycc through as a --cfg=bun_tinycc rustflag, and both the tcc_externs! macro's extern/stub selection and ENABLE_TINYCC (the runtime gate in bun:ffi's cc()) now key on that one cfg instead of three hand-maintained copies of the android/freebsd platform list.

Files touched: scripts/build/rust.ts (adds the cfg to rustflags), scripts/build/buildOptionsRs.ts (emits cfg!(bun_tinycc) instead of a hard-coded target predicate), src/tcc_sys/tcc.rs (gates on bun_tinycc instead of target_os), Cargo.toml (registers the cfg for bare cargo), src/bun_core/env.rs (doc comment only), and a new test block in test/internal/source-lints/build-rust.test.ts.

Security risks

None. This is build-configuration plumbing with no runtime code-path changes for shipped binaries. No user input, network, crypto, or auth surface.

Level of scrutiny

Medium — build infrastructure is load-bearing, but this change is a copy of an established pattern. The exact same shape (--check-cfg always, --cfg conditionally, registered in root unexpected_cfgs, gates both #[cfg] code and a cfg!() constant) already exists three times over for bun_asan, bun_debug, and socket_fault_injection in the lines immediately adjacent to the additions. The PR description explicitly addresses the two non-obvious consequences (RUSTFLAGS change triggers one full rebuild; bare cargo check now compiles the stub arm, matching the other cfgs' convention).

Other factors

  • The new source-lint tests are configure-time only (no cargo invocation), use tempDir from harness, and pin the full chain: generated build_options.rs → rustflags → Cargo.toml registration → tcc_externs! gating → no stray fn tcc_* elsewhere in src/. The tests are hermetic and would fail on main.
  • I confirmed bun_tcc_sys inherits workspace lints, so the Cargo.toml unexpected_cfgs entry actually reaches the crate that uses the cfg.
  • The runtime consumer (ffi_body.rs:993) still reads bun_core::Environment::ENABLE_TINYCC and early-returns; that constant now derives from the same cfg the externs gate on, so the two cannot disagree.
  • No CODEOWNER paths, no outstanding reviewer comments, no prior automated review from me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant