Skip to content

install: filter optionalDependencies by libc (glibc/musl) - #31123

Closed
Jarred-Sumner wants to merge 2 commits into
mainfrom
claude/install-libc-filtering
Closed

Jarred-Sumner wants to merge 2 commits into
mainfrom
claude/install-libc-filtering

Conversation

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

What

bun install filters optional dependencies by the npm os and cpu fields, but ignores the libc field. As a result, on a glibc Linux host bun install can pull a package variant published only for musl (and vice versa) instead of the matching one — npm/yarn install only the matching variant.

Repro

A package whose optionalDependencies are glibc/musl variants of the same native binary:

{
  "optionalDependencies": {
    "pkg-linux-x64-glibc": "1.0.0",
    "pkg-linux-x64-musl": "1.0.0"
  }
}

where the two sub-packages declare "libc": ["glibc"] and "libc": ["musl"]. On a glibc host, bun install installs both; it should install only the glibc one.

Fix

Thread libc through the same filtering path os/cpu already use:

  • record libc on Package.Meta, reusing an existing padding byte — the binary lockfile layout and format version are unchanged, and a lockfile written before this change reads back as unconstrained and keeps installing
  • copy it from the manifest in Package::from_npm (and the metadata-refresh path), and persist it in the text lockfile
  • check it in Meta::is_disabled; an unset/empty libc is treated as unconstrained, so packages without a libc field are unaffected
  • derive Libc::CURRENT from target_env at compile time (a musl build runs on musl, a gnu build on glibc) — consistent with how OperatingSystem::CURRENT/Architecture::CURRENT derive from target_os/target_arch — replacing a hardcoded glibc
  • add a --libc flag mirroring --os/--cpu, for parity and cross-target installs

Tests

test/cli/install/bun-install-cpu-os.test.ts gains libc cases: filtering by --libc, the combined os+cpu+libc case (a glibc target must not pull a musl-only variant), and the invalid-value error.

`bun install` filtered optional dependencies by `os` and `cpu` but ignored
the npm `libc` field, so a package published only for `musl` could be
installed on a glibc system, and vice versa.

Thread `libc` through the same path `os`/`cpu` already use:

- record `libc` on `Package.Meta` (reusing an existing padding byte, so the
  binary lockfile layout and format version are unchanged; a lockfile
  written before this reads back as unconstrained and still installs)
- copy it from the manifest in `Package::from_npm` and the refresh path
- check it in `Meta::is_disabled`, treating an unset/empty `libc` as
  unconstrained so packages without a `libc` field keep installing
- read/write it in the text lockfile
- derive the host libc from `target_env` at compile time (a musl build runs
  on musl, a gnu build on glibc), replacing the hardcoded `Libc::CURRENT`
- add a `--libc` flag mirroring `--os`/`--cpu`

Adds tests covering `--libc` filtering, the combined os+cpu+libc case, and
the invalid-value error.
@robobun

robobun commented May 20, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 2:21 AM PT - May 20th, 2026

❌ @autofix-ci[bot], your commit 4f55b07 has 1 failures in Build #56381 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 31123

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

bun-31123 --bun

@coderabbitai

coderabbitai Bot commented May 20, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds --libc command-line flag support to enable filtering optional dependencies by their libc variant (glibc vs musl), mirroring the existing --cpu and --os filtering mechanisms. The implementation spans type definitions, CLI parsing, package metadata schema changes, and integration into the dependency resolution and lockfile serialization workflows.

Changes

Libc-based optional dependency filtering

Layer / File(s) Summary
Core Libc type with compile-time host detection
src/install_types/resolver_hooks.rs
Libc::CURRENT is selected at compile-time: MUSL when building for Linux musl target, otherwise GLIBC. Adds Libc::CURRENT_NAME string constant.
CLI argument parsing for --libc flag
src/install/PackageManager/CommandLineArguments.rs
New --libc <STR>... option in shared CLI docs, libc field in CommandLineArguments struct initialized to Libc::CURRENT, and parse logic supporting negation/wildcard (*) handling with validation.
Libc option propagation through Options struct
src/install/PackageManager/PackageManagerOptions.rs
New libc field in Options struct with Libc::CURRENT default; Options::load copies CLI-provided libc into the options alongside CPU and OS.
Package metadata schema with libc field and disable logic
src/install/lockfile/Package/Meta.rs
Meta struct gains libc: Libc field, ABI padding adjusted, default initializes to Libc::NONE. Meta::is_disabled updated to accept libc and treats packages disabled when libc is constrained and mismatched. clone_into copies the field.
Package disable detection and metadata population
src/install/lockfile/Package.rs, src/install/lockfile.rs
Package::is_disabled updated to accept and forward libc to meta.is_disabled. from_npm populates package.meta.libc from npm metadata. Lockfile::is_resolved_dependency_disabled signature updated to accept libc. Metadata propagation during yarn/npm/pnpm migration now copies libc when unset.
Lockfile serialization and deserialization of libc metadata
src/install/lockfile/bun.lock.rs
Conditional JSON serialization of libc in bun.lock when constrained (neither NONE nor ALL), and JSON parsing to hydrate pkg.meta.libc during lockfile read.
Integration into install workflow and filtering
src/install/PackageManager/PackageManagerLifecycle.rs, src/install/lockfile/Tree.rs, src/install/lockfile/printer/tree_printer.rs
determine_preinstall_state evaluates package disability with libc, Tree filtering applies libc constraint during disabled-package checks, and tree_printer passes libc when determining filtered dependencies.
CLI tests for --libc flag behavior
test/cli/install/bun-install-cpu-os.test.ts
Tests verify basic libc-based dependency selection (glibc vs musl), combined filtering with --os and --cpu, and error handling for invalid libc values.

Suggested reviewers

  • RiskyMH
  • dylan-conway
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding libc filtering to optional dependency resolution, which is the central feature of this PR.
Description check ✅ Passed The PR description comprehensively covers both required sections: it explains what the PR does (filtering optionalDependencies by libc field), provides a concrete reproduction scenario, and documents the implementation fix with technical details. Test coverage is also mentioned.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@github-actions

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. Add support for libc in package.json to filter package to download #16282 - Requests support for the libc field in package.json to filter optional dependency downloads (glibc vs musl), which this PR directly implements
  2. Vite/PostCSS/lightningcss build issue with Alpine docker image #16915 - Alpine/musl Docker containers get the glibc variant of lightningcss because bun install ignores the libc field in optionalDependencies

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #16282
Fixes #16915

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Add libc field to Package.Meta and simplify struct #22965 - Adds libc field to Package.Meta, --libc CLI flag, and libc-based optional dependency filtering — same feature
  2. [bun install] manual os overide flags + basic level of libc filtering for linux #21174 - Adds libc filtering for optional dependencies during bun install and CLI os/cpu/libc override flags — same feature

🤖 Generated with Claude Code

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/install/lockfile/Tree.rs (1)

597-621: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Verbose skip reason misses libc-only mismatches.

When a package is disabled solely by libc, verbose mode emits no mismatch reason. Add a libc branch so filtered installs remain diagnosable.

Suggested patch
             } else if !meta.arch.is_match(manager.options.cpu) {
                 Output::pretty_errorln(format_args!(
                     "<d>Skip installing<r> <b>{}<r> <d>- cpu mismatch<r>",
                     bstr::BStr::new(name)
                 ));
+            } else if !meta.libc.is_match(manager.options.libc) {
+                Output::pretty_errorln(format_args!(
+                    "<d>Skip installing<r> <b>{}<r> <d>- libc mismatch<r>",
+                    bstr::BStr::new(name)
+                ));
             }
🤖 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 `@src/install/lockfile/Tree.rs` around lines 597 - 621, The verbose skip branch
currently reports only cpu/os mismatches but omits libc-only cases; update the
conditionals around pkg_metas[pkg_id as usize] in Tree.rs to include libc
checks: extend the combined mismatch check to consider !meta.os.is_match(...),
!meta.arch.is_match(...), and !meta.libc.is_match(manager.options.libc) (so the
first branch covers multi-way mismatches), and add an else if that checks
!meta.libc.is_match(manager.options.libc) and emits Output::pretty_errorln with
a "<d>Skip installing<r> <b>{}<r> <d>- libc mismatch<r>" message (use the
existing bstr::BStr::new(name) pattern); use the same pattern for any additional
combined messages you deem necessary so libc-only and combination cases are
diagnosable.
🤖 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.

Outside diff comments:
In `@src/install/lockfile/Tree.rs`:
- Around line 597-621: The verbose skip branch currently reports only cpu/os
mismatches but omits libc-only cases; update the conditionals around
pkg_metas[pkg_id as usize] in Tree.rs to include libc checks: extend the
combined mismatch check to consider !meta.os.is_match(...),
!meta.arch.is_match(...), and !meta.libc.is_match(manager.options.libc) (so the
first branch covers multi-way mismatches), and add an else if that checks
!meta.libc.is_match(manager.options.libc) and emits Output::pretty_errorln with
a "<d>Skip installing<r> <b>{}<r> <d>- libc mismatch<r>" message (use the
existing bstr::BStr::new(name) pattern); use the same pattern for any additional
combined messages you deem necessary so libc-only and combination cases are
diagnosable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 75e151c4-2bb1-4092-9844-928196c56e0c

📥 Commits

Reviewing files that changed from the base of the PR and between b3d086e and 4f55b07.

📒 Files selected for processing (15)
  • src/install/PackageManager/CommandLineArguments.rs
  • src/install/PackageManager/PackageManagerLifecycle.rs
  • src/install/PackageManager/PackageManagerOptions.rs
  • src/install/lockfile.rs
  • src/install/lockfile/Package.rs
  • src/install/lockfile/Package/Meta.rs
  • src/install/lockfile/Tree.rs
  • src/install/lockfile/bun.lock.rs
  • src/install/lockfile/printer/tree_printer.rs
  • src/install_types/resolver_hooks.rs
  • test/cli/install/bun-install-cpu-os.test.ts
  • test/cli/install/dep-glibc-1.0.0.tgz
  • test/cli/install/dep-linux-x64-glibc-1.0.0.tgz
  • test/cli/install/dep-linux-x64-musl-2.0.0.tgz
  • test/cli/install/dep-musl-2.0.0.tgz

Comment on lines +70 to 76
pub fn is_disabled(&self, cpu: Architecture, os: OperatingSystem, libc: Libc) -> bool {
!self.arch.is_match(cpu)
|| !self.os.is_match(os)
// `libc` is NONE both for packages without a `libc` field and for
// lockfiles written before libc was recorded; treat that as unconstrained.
|| (self.libc != Libc::NONE && !self.libc.is_match(libc))
}

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.

🔴 get_native_binlink_replacement_package_id still matches only on meta.arch + meta.os and ignores the new meta.libc, so for a package with paired -gnu/-musl optionalDependencies it can return the wrong-libc variant — which after this PR is filtered out by is_disabled and never installed, leaving the binlink pointing at nothing. Add a target_libc: npm::Libc parameter, gate line 126 with && (meta.libc == npm::Libc::NONE || meta.libc.is_match(target_libc)), and thread manager.options.libc through the three call sites (PackageInstaller.rs:543, isolated_install/Installer.rs:2208, postinstall_optimizer.rs:219).

Extended reasoning...

What the bug is

PostinstallOptimizer::get_native_binlink_replacement_package_id (src/install/postinstall_optimizer.rs:103-132) selects which platform-specific optionalDependency should replace a meta-package's bin (the optimization that lets bun skip esbuild's postinstall by linking directly to @esbuild/linux-x64's binary). It picks the replacement by iterating the lockfile's resolution list and returning the first entry where:

meta.arch.is_match(target_cpu) && meta.os.is_match(target_os)

There is no meta.libc check. This PR adds libc to Meta and threads it through is_disabled, the tree filter, the preinstall-state check, and the printer — but this function and its three callers were missed.

The code path

All three call sites read manager.options.cpu / manager.options.os but not the newly-added manager.options.libc:

  • src/install/PackageInstaller.rs:543-551 — hoisted installer, passes pkg_resolutions_lists[package_id].get(...) (the lockfile resolution slice for the meta-package, which contains all variants regardless of whether they were filtered at install time)
  • src/install/isolated_install/Installer.rs:2208-2215 — isolated installer, same
  • src/install/postinstall_optimizer.rs:219-224 — should_ignore_lifecycle_scripts, which decides whether to skip the postinstall in the first place

Why nothing else prevents it

The function iterates the meta-package's resolution slice from the lockfile, not the set of packages that survived is_disabled filtering. Line 123 requires the candidate to declare a specific arch and a specific os (!= ALL), and line 126 checks they match the target — but a @foo/linux-x64-gnu variant and a @foo/linux-x64-musl variant both declare cpu: ["x64"], os: ["linux"] and both pass on a linux-x64 host. Whichever appears first in the resolution slice wins, regardless of host libc.

Step-by-step proof

Take a package native-pkg opted into nativeDependencies (or hypothetically added to the default list) with:

"optionalDependencies": {
  "@native-pkg/linux-x64-gnu":  "1.0.0",  // cpu:[x64] os:[linux] libc:[glibc]
  "@native-pkg/linux-x64-musl": "1.0.0"   // cpu:[x64] os:[linux] libc:[musl]
}

On an Alpine (musl) linux-x64 host:

  1. Both variants are resolved into the lockfile (lockfiles are portable).
  2. Install filtering (this PR's change): Meta::is_disabled now returns true for the -gnu variant (meta.libc == GLIBC, host libc == MUSL), so is_filtered_dependency_or_workspace skips it and it is not written to node_modules. Before this PR, both were installed.
  3. Binlink selection: get_native_binlink_replacement_package_id iterates native-pkg's resolutions [gnu, musl]. For gnu: arch == X64 != ALL ✓, os == LINUX != ALL ✓, arch.is_match(X64) ✓, os.is_match(LINUX) ✓ → returns the -gnu package_id.
  4. The installer then creates a binlink targeting @native-pkg/linux-x64-gnu, which was never installed — broken bin.

Before this PR step 2 didn't filter by libc, so whichever variant the optimizer picked was at least present on disk; this PR makes the inconsistency observable.

Impact

The default native-binlink list is currently ["esbuild", "@anthropic-ai/claude-code"] (postinstall_optimizer.rs:25-30), neither of which ships glibc/musl-split variants today, so out-of-the-box installs are unaffected. The bug bites users who add a libc-split package (e.g. @swc/core, @rollup/rollup-*, @biomejs/biome) to nativeDependencies in package.json — exactly the population this PR is for. It's also a latent hazard for anyone extending the default list. Given the PR's stated goal is "thread libc through the same filtering path os/cpu already use", this is squarely in scope.

Fix

Add a target_libc: npm::Libc parameter and mirror Meta::is_disabled:

if meta.arch.is_match(target_cpu)
    && meta.os.is_match(target_os)
    && (meta.libc == npm::Libc::NONE || meta.libc.is_match(target_libc))
{
    return Some(resolution);
}

Then thread manager.options.libc through PackageInstaller.rs:543, isolated_install/Installer.rs:2208, and pass it down through should_ignore_lifecycle_scripts to the internal call at postinstall_optimizer.rs:219.

Comment thread src/install/lockfile/Tree.rs Outdated
let parent_res = &pkg_resolutions[parent_pkg_id as usize];

if pkg_metas[pkg_id as usize].is_disabled(manager.options.cpu, manager.options.os) {
if pkg_metas[pkg_id as usize].is_disabled(manager.options.cpu, manager.options.os, manager.options.libc) {

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.

🟡 Two minor follow-ups now that is_disabled() checks libc: (1) the verbose-logging block immediately below (lines ~601-616) only has branches for "cpu & os mismatch" / "os mismatch" / "cpu mismatch", so a package skipped solely for a libc mismatch prints no "Skip installing" message under --verbose — adding an else branch for "libc mismatch" would give parity; (2) the doc comment on BuilderMethod::Filter (~line 460) still reads "'libc' (TODO)" — the TODO is now done and can be dropped.

Extended reasoning...

What

Two small cleanups in src/install/lockfile/Tree.rs that fall out of this PR's libc filtering, both cosmetic / non-functional:

(1) Missing verbose-log branch for libc-only mismatch

At line 597, is_disabled() now returns true when meta.libc doesn't match the target libc. But the verbose-logging block immediately below (lines ~601-616) still only checks os/cpu:

if !meta.os.is_match(manager.options.os) && !meta.arch.is_match(manager.options.cpu) {
    // "- cpu & os mismatch"
} else if !meta.os.is_match(manager.options.os) {
    // "- os mismatch"
} else if !meta.arch.is_match(manager.options.cpu) {
    // "- cpu mismatch"
}
// no branch for libc

When a package is disabled purely because of libc (os matches AND cpu matches, but meta.libc doesn't), none of the three conditions fire, so no "Skip installing" line is printed under --verbose. The package is still correctly skipped (return true at line 618) — this is purely a verbose-output completeness gap.

Step-by-step example: on a linux-x64-glibc host, install a package whose optionalDependencies includes pkg-linux-x64-musl declaring "os": ["linux"], "cpu": ["x64"], "libc": ["musl"] (exactly the scenario this PR targets). Run bun install --verbose:

  1. is_disabled(x64, linux, glibc) → arch.is_match(x64) ✓, os.is_match(linux) ✓, libc != NONE && !libc.is_match(glibc) → returns true.
  2. Enter the verbose block. !meta.os.is_match(linux) → false; !meta.arch.is_match(x64) → false. All three branches are skipped.
  3. Function returns true (package correctly filtered) but nothing was logged, whereas a darwin-only or arm64-only sibling would have printed "Skip installing … - os/cpu mismatch".

Fix: add a final else (or else if !meta.libc.is_match(manager.options.libc)) branch printing "- libc mismatch".

(2) Stale TODO in BuilderMethod::Filter doc comment

Around line 460, the doc comment on BuilderMethod::Filter reads:

We skip dependencies based on 'os', 'cpu', 'libc' (TODO), and omitted dependency types

This PR implements the libc check at line 597, so the "(TODO)" marker is now stale and can simply be removed.

Impact

Both are nits with zero functional impact — filtering behavior is correct. (1) is a minor --verbose UX inconsistency introduced by this PR; (2) is pure doc-comment cleanup directly in scope of what the PR implements.

Comment on lines +919 to 924
// The libc of the running binary: a musl-target build can only run on musl,
// a gnu-target build on glibc, so the compile-time target_env is the host libc.
#[cfg(all(target_os = "linux", target_env = "musl"))]
pub const CURRENT: Self = Self(Self::MUSL);
#[cfg(not(all(target_os = "linux", target_env = "musl")))]
pub const CURRENT: Self = Self(Self::GLIBC);

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.

🟡 On non-Linux targets (macOS, Windows), Libc::CURRENT falls through to Self(Self::GLIBC), but npm only applies the libc check on Linux — on other platforms it ignores the field entirely. A package declaring "libc": ["musl"] without an os constraint would now be filtered out by bun on macOS/Windows where npm would install it. In practice such packages almost always also set "os": ["linux"] so the OS filter catches them first, but for npm parity the non-Linux fallback should be Libc::ALL (or gate the libc check in Meta::is_disabled on target_os = "linux").

Extended reasoning...

What the issue is

The new Libc::CURRENT constant uses two #[cfg] arms: one for all(target_os = "linux", target_env = "musl") → MUSL, and a catch-all not(...) arm → GLIBC. On Linux that's correct (a gnu-target build runs on glibc, a musl-target build on musl). But on macOS and Windows there is no libc in the npm sense, and the catch-all still resolves to GLIBC. Since this PR also wires Libc::CURRENT into Meta::is_disabled (via Options::libc → is_disabled(cpu, os, libc)), the libc check is now active on every platform.

How it diverges from npm

Per the npm docs and the npm-install-checks implementation, the libc field is only validated when process.platform === 'linux'; on any other platform the check is skipped entirely (libcOk = true). So under npm, a package that declares only "libc": ["musl"] (with no os constraint) installs fine on macOS or Windows.

Concrete walk-through

Consider running bun install on macOS with an optional dependency whose package.json contains "libc": ["musl"] and no os field:

  1. Options::default() / CLI defaults set self.libc = Libc::CURRENT, which on macOS is Libc(GLIBC) (resolver_hooks.rs:921–924).
  2. Package::from_npm copies package_version.libc → meta.libc = Libc(MUSL).
  3. Meta::is_disabled evaluates the new clause: self.libc != Libc::NONE (true, it's MUSL) && !self.libc.is_match(libc) → !(MUSL & GLIBC != 0) → !(0b100 & 0b010 != 0) → !false → true.
  4. The package is treated as disabled and skipped.

Under npm on the same macOS host, checkPlatform sees process.platform !== 'linux', skips the libc comparison, and installs the package. So this is a newly-introduced behavioral divergence — before this PR is_disabled never looked at libc at all.

Why nothing else prevents it

The guard in is_disabled (self.libc != Libc::NONE) only handles the package side (no libc declared / old lockfile). There's no corresponding guard on the host side for "this platform has no meaningful libc". The os check would normally catch real-world musl packages first (they all declare "os": ["linux"]), but that's a convention, not a guarantee.

Impact and fix

Practical impact is low: every real musl-only package on npm (@swc/core-linux-*-musl, @esbuild/linux-*, sharp variants, etc.) also declares "os": ["linux"], so the OS filter already excludes them on macOS/Windows and the libc check is never the deciding factor. This only bites a package that sets libc without os, which is arguably misconfigured. Still, since the PR's stated goal is npm parity and the fix is one line, it's worth tightening: either add a third #[cfg(not(target_os = "linux"))] pub const CURRENT: Self = Self::ALL; arm (so is_match always passes on non-Linux), or gate the libc clause in Meta::is_disabled on Linux.

Comment thread src/install/lockfile.rs
Comment on lines +1654 to +1656
if pkg_meta.libc == Npm::Libc::NONE {
pkg_meta.libc = pkg.package.libc;
}

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.

🟡 A few more spots that thread os/cpu but weren't updated for libc (all default to Libc::NONE = unconstrained, so no false filtering, just parity gaps): (1) pnpm-lock.yaml migration — src/install/pnpm.rs:918-924 parses os/cpu per package but still has // TODO: libc, and since pnpm.rs:1152 calls fetch_necessary_package_metadata_after_yarn_or_pnpm_migration::<false> the new backfill at lockfile.rs:1654 doesn't run for pnpm, so libc constraints from pnpm-lock.yaml are dropped; (2) Package::from_package_json (Package.rs:687-688) copies package_json.arch/os but not libc, and PackageJsonView (resolver_hooks.rs:1640-1641) has fn arch()/fn os() but no fn libc(), so folder/local deps ignore a package.json libc field; (3) the debug JSON dumper (lockfile_json_stringify_for_debugging.rs:379-401) emits arch/os arrays but not libc (debug-only, lowest priority).

Extended reasoning...

What these are

This PR's stated scope is "thread libc through the same filtering path os/cpu already use", and it does so for Meta::is_disabled, from_npm, bun.lock serialization, the tree filter, the preinstall-state check, the printer, and the yarn-migration backfill. There are three remaining places where os/cpu are read or emitted side-by-side and libc was not added. None cause false filtering (the default Meta.libc == Libc::NONE is treated as unconstrained by is_disabled), so these are completeness nits rather than functional bugs.

(1) pnpm-lock.yaml migration — src/install/pnpm.rs:924

The pnpm-lock.yaml parser reads per-package os and cpu at lines 918-923 but leaves // TODO: libc at line 924. pnpm-lock.yaml does record per-package libc: arrays. The fix is a one-liner mirroring the two cases above it:

if let Some(libc_expr) = package_obj.get(b"libc") {
    pkg.meta.libc = npm::negatable_from_json::<npm::Libc>(&libc_expr)?;
}

Why the new backfill does not cover this: the backfill this PR adds at lockfile.rs:1654-1656 sits inside if UPDATE_OS_CPU { ... } (line 1645), and pnpm.rs:1152 calls fetch_necessary_package_metadata_after_yarn_or_pnpm_migration::<false> — i.e. UPDATE_OS_CPU = false. Only yarn.rs:2042 passes <true>. So for pnpm migration the libc backfill is never reached.

Step-by-step: migrate a pnpm-lock.yaml whose packages: section has an entry with libc: [musl]. (a) pnpm.rs parses os/cpu, skips libc → pkg.meta.libc stays Libc::NONE. (b) fetch_necessary_package_metadata_after_yarn_or_pnpm_migration::<false> runs; UPDATE_OS_CPU is false, so the pkg_meta.libc = pkg.package.libc line is skipped. (c) On a glibc host, Meta::is_disabled evaluates self.libc != Libc::NONE → false, so the libc clause is bypassed and the musl-only package installs. This is the pre-PR behavior (both variants installed), not a regression — but it is the one item here with an observable effect, and it's a 3-line fix squarely in scope.

(2) from_package_json / PackageJsonView — Package.rs:687-688, resolver_hooks.rs:1640-1641

from_npm now sets package.meta.libc = package_version.libc (Package.rs:945), but the parallel from_package_json path still only copies package_json.arch and package_json.os (lines 687-688). The PackageJsonView trait at resolver_hooks.rs has fn arch() and fn os() but no fn libc(), and the resolver's PackageJSON struct (resolver/package_json.rs:205-206) likewise has no libc field. So folder/link dependencies and the auto-install resolver path ignore any "libc" field in their package.json.

Step-by-step: a folder dependency whose package.json declares "libc": ["musl"] on a glibc host: from_package_json leaves meta.libc = Libc::NONE → is_disabled short-circuits the libc check → package installs. Again no false filtering, just a missed filter. Real-world impact is very low — libc-split packages are npm-published native binaries that go through from_npm (which IS updated), not folder deps — but it is a parity gap with the os/cpu fields directly above. This one is a multi-file change (struct + trait + parser + from_package_json), so reasonable to defer.

(3) Debug JSON dumper — lockfile_json_stringify_for_debugging.rs:379-401

The debug stringifier emits "arch" (379-391) and "os" (393-404) arrays from pkg.meta but has no block for pkg.meta.libc. Now that bun.lock serializes libc (bun.lock.rs:1054-1062), the debug dump no longer reflects the full Meta it's printing.

Addressing the objection that this is below the bar: it's fair that this is debug-only, zero-functional-impact, and not a "filtering path". I'm including it only because (a) it sits literally next to the arch/os blocks it would mirror, (b) bun.lock.rs was updated for the same reason, and (c) it's bundled here with two in-scope items rather than filed standalone. If you'd rather skip it, the first two stand on their own.

Impact and fix

All three default to unconstrained, so the worst case is the pre-PR behavior (no filtering) on those paths — never an incorrectly-skipped package. (1) is a 3-line addition with an observable effect on pnpm→bun migration and is the most worth folding in; (2) is a parity gap requiring touching 3-4 files; (3) is purely cosmetic.

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

From when I looked at this some time ago, the abbreviated registry api doesnt provide libc. If you have minimumReleaseAge, then it'll work as it needs full.

In my older pr I think I had a simple word check for common keys in libc like packages to then use normal api.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Rebased onto current main in #38786 (same design: libc in the Meta padding byte, Libc::CURRENT from the target env, --libc flag, libc in bun.lock), plus two things that turned out to be needed for it to change anything against registry.npmjs.org: the abbreviated manifest never includes libc, so #38786 infers it from the package name for packages that declare os/cpu (as pnpm does) and also applies that to already-written lockfiles; and package-lock.json / pnpm-lock.yaml migrations carry the field over. This one can probably be closed in favor of it.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

#38797 picks this up on current main and also covers the point raised above about the abbreviated registry document not carrying libc: optional dependencies now request the full document, so the filter has the field for real packages (rollup, swc, sharp, ...), and bun.lock/bun.lockb record it.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Closing this one in favor of #38797, which carries its design forward: libc lives in the former padding byte of Package.Meta, Meta::is_disabled checks it, bun.lock records it, Libc::CURRENT comes from target_env, and --libc mirrors --os/--cpu. The commit there credits you with a Co-authored-by trailer.

What it adds is the part this branch could not get to on its own: the abbreviated registry document bun installs from never contains libc (checked against registry.npmjs.org: 0 of 212 versions of @rollup/rollup-linux-x64-musl, while the full document has it for all of them), so #38797 requests the full document for optional dependencies, the way pnpm does, and then filters and records the field; because the field is only fetched for optional dependencies, it is also only enforced on those. It also fixes the native binlink optimizer picking the glibc variant on musl. This branch no longer applies to main after the install refactors. Feel free to reopen if you would rather land it this way.

@robobun robobun closed this Aug 15, 2026
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.

3 participants