Skip to content

Archives from separate cargo invocations can bundle different tokio builds off one Cargo.lock; the link guard catches it but two agents hit it today in unrelated work #10671

Description

@proggeramlug

Two independent agents hit this today, hours apart, in unrelated work. Filing it as a build-system issue
because it is not a one-off and it produces a confusing failure.

The problem

Two static archives built in separate cargo invocations can carry different tokio compilations even off an
identical Cargo.lock
. Linking them together then fails.

Perry's own linker detects and refuses this (crates/perry/src/commands/compile/library_search.rs,
runtime_compat.rs link guard), which is good — it fails loudly rather than producing a subtly broken binary. But
the error surfaces far from the cause, and the fix is non-obvious unless you already know the rule.

The two sightings

  1. Package-compilability probe. Compiling pg auto-triggered a perry-ext-pg build (it bridges to
    sqlx::postgres + tokio) in its own cargo invocation; the resulting libperry_ext_pg.a bundled a different
    tokio than the existing libperry_stdlib.a. Resolved by rebuilding coherently:
    cargo build --profile perry-dev -p perry -p perry-runtime-static -p perry-stdlib-static -p perry-ext-pg
  2. PR fix(http): rebuild client-pump stdlib for node:http under PERRY_NO_AUTO_OPTIMIZE #10667. Its first version rebuilt perry-stdlib-static without perry-ext-http, tripped the
    runtime_compat.rs guard, and had to rebuild both together in one invocation.

Why it is worth fixing rather than documenting

CLAUDE.md already warns that dropping a -p changes cargo feature unification, and the fix-agent brief tells
agents to keep the package set identical across builds. Both sightings happened anyway, to agents that had read
those warnings — because the failure mode is triggered by an auto-triggered build (case 1) or by a reasonable
partial rebuild (case 2), not by carelessness.

Options worth weighing: have the ext-crate auto-build join the parent invocation rather than spawning its own;
have the link guard's error name the required cargo build -p ... line explicitly; or record a build fingerprint
in each archive so the mismatch is reported as "built in a different invocation" rather than a tokio symbol clash.

Related

The same family as the stale-.a trap CLAUDE.md documents under "Verifying a runtime change" — where rebuilding
the rlib crates without the -static wrappers silently links a stale archive and makes both arms of an A/B behave
identically. That one produces a vacuous pass; this one produces a confusing failure. Both come from archives
and invocations not lining up.

Activity

  1. added
    package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindings
    on Sep 18, 2026
  2. proggeramlug commented on Sep 18, 2026

    @proggeramlug
    ContributorAuthor

    Third sighting today, in a third piece of unrelated work: the agent fixing #10437 hit it compiling pg end-to-end — perry-ext-net and perry-ext-pg wrapper archives built in separate invocations, resolved the same way (one cargo invocation covering all of them).

    That agent attributed it to #507 and #7629. Having checked, it is neither — both are closed and describe different failure modes: #507 is LTO dead-stripping tokio's CONTEXT statics, #7629 is a deterministic SIGABRT in one gap test. Same neighbourhood (tokio inside ext-crate archives), different mechanism. This issue — archives from separate cargo invocations carrying different tokio builds off one Cargo.lock — remains distinct and open.

    Three independent hits in one day, each by someone who had read the warnings, is the argument for fixing it in the build system rather than documenting it harder.

  3. proggeramlug commented on Sep 22, 2026

    @proggeramlug
    ContributorAuthor

    Concrete reproduction of this failure mode, found while re-validating four packages after their original compile blockers landed (mongodb #10758, ethers #10757, node-fetch #10759, nodemailer #10798 — all closed).

    All four need both the http-client-pump well-known recipe (for node:http/node:https) and perry-ext-net. With PERRY_NO_AUTO_OPTIMIZE=1, none of them can link:

    Error: error: the wrapper archive(s) below bundle a DIFFERENT tokio compilation than the stdlib archive they would be linked with.
      target/perry-no-auto-http-pump/release/libperry_stdlib.a bundles tokio-1200aeae2e77614b
      libperry_ext_net.a bundles tokio-ef1329cc84fee118
    

    Root cause is exactly what the error message + this issue title describe: crates/perry/src/commands/compile/optimized_libs/no_auto.rs's build_http_client_pump_stdlib always runs

    cargo build --release -p perry-stdlib-static -p perry-ext-http --features perry-stdlib/external-http-client-pump
    

    — deliberately not including perry-ext-net (per its own doc comment, to avoid forcing every no-auto program to carry libperry_ext_http.a). That's fine when a program only needs http-client-pump. It's not fine when the program also needs net (or any other well-known routing) in the same binary: perry-ext-net comes from a separately-built archive (either the prebuilt target/release/libperry_ext_net.a, or perry's own well-known: routing net → rebuilding perry-ext-net with shared tokio step), and feature-unification differences between that build and the narrow http-client-pump recipe's build produce a different tokio content hash even at the same pinned version — so the link-time guard (correctly) refuses every time, deterministically, not just as a caching artifact. I confirmed this by deleting target/perry-no-auto-http-pump and rebuilding from scratch (same hash 1200aeae2e77614b both times), and separately by manually pre-building a fully coherent stdlib+http+net+... set directly into that target dir — Perry's own http-client-pump (no-auto) step re-invokes its narrower recipe anyway on the next compile and clobbers it back to the incoherent state.

    Workaround that does work: unset PERRY_NO_AUTO_OPTIMIZE and let the full auto-optimize coherent rebuild run. Slower, but it unifies tokio across the whole feature set in one invocation and all four packages linked and ran (hitting separate, further-downstream bugs — #11043, #11044, #10809, #11045 — none of them tokio-coherence related).

    If a fix lands here, the direct check is: compile any of mongodb/ethers/node-fetch/nodemailer with PERRY_NO_AUTO_OPTIMIZE=1 and confirm it links (not just that the tokio hashes match — confirm the binary also runs past its first socket operation, since a silently-adopted stale archive would still "link").

  4. proggeramlug commented on Sep 23, 2026

    @proggeramlug
    ContributorAuthor

    Re-tested after turnloop landed (b77aba6343, tokio removed from perry-runtime). This still reproduces, and the guard now names three tokio compilations instead of two.

    Tested on origin/main = 2f854511e9 (v0.5.1640), compiling real-source mongodb@7.5.0 with PERRY_NO_AUTO_OPTIMIZE=1. Fails after 13m43s with the same coherence guard, naming:

    libperry_stdlib.a       bundles tokio-1200aeae
    libperry_ext_mongodb.a  bundles tokio-d03f425a
    libperry_ext_net.a      bundles tokio-2cbb3241
    

    So turnloop removed tokio from perry-runtime, but tokio is still separately bundled per cargo invocation into perry-stdlib and each perry-ext-* wrapper, and no_auto.rs still never builds them in one invocation. The workaround remains necessary: leave PERRY_NO_AUTO_OPTIMIZE unset so auto-optimize produces a coherent set.

    Worth stating plainly because the natural assumption after turnloop is the opposite: tokio leaving the runtime does not fix this, and the count of distinct compilations went up, since perry-ext-net is now a separate participant rather than sharing the runtime's copy.

    The guard itself is behaving correctly — it is refusing a binary that would abort at its first socket rather than shipping it. The defect is that no_auto.rs's recipe cannot produce a coherent set for any package needing both the http-client-pump recipe and perry-ext-net, which is at least mongodb, ethers, node-fetch and nodemailer.

    Why this matters more now than before

    Removing the ioredis, mongodb and http/net bindings — the remaining work in the binding-removal campaign — deletes three of the six crates that still carry tokio. So the campaign and shrinking the tokio surface are largely the same work, and this issue is what makes those packages expensive to validate in the meantime: every probe of them must either use auto-optimize or hit this guard.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindings

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions