Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion src/install/PackageManager/CommandLineArguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ const SHARED_PARAMS: &[ParamType] = &[
clap::param!(
"--os <STR>... Override operating system for optional dependencies (e.g., linux, darwin, * for all)"
),
clap::param!(
"--libc <STR>... Override libc for optional dependencies (e.g., glibc, musl, * for all)"
),
clap::param!("-h, --help Print this help menu"),
];

Expand Down Expand Up @@ -440,9 +443,10 @@ pub struct CommandLineArguments {
pub audit_level: Option<AuditLevel>,
pub audit_ignore_list: &'static [&'static [u8]],

// CPU and OS overrides for optional dependencies
// CPU, OS, and libc overrides for optional dependencies
pub cpu: Npm::Architecture,
pub os: Npm::OperatingSystem,
pub libc: Npm::Libc,
}

impl Default for CommandLineArguments {
Expand Down Expand Up @@ -526,6 +530,7 @@ impl Default for CommandLineArguments {

cpu: Npm::Architecture::CURRENT,
os: Npm::OperatingSystem::CURRENT,
libc: Npm::Libc::CURRENT,
}
}
}
Expand Down Expand Up @@ -1310,6 +1315,33 @@ Full documentation is available at <magenta>https://bun.com/docs/cli/pm#scan<r>.
cli.os = os_negatable.combine();
}

// Parse multiple --libc flags and combine them using Negatable
let libc_values = args.options(b"--libc");
if !libc_values.is_empty() {
let mut libc_negatable = Npm::Libc::NONE.negatable();
for libc_str in libc_values {
// apply() already handles "any" as wildcard and negation with !
libc_negatable.apply(libc_str);

// Support * as an alias for "any"
if *libc_str == *b"*" {
libc_negatable.had_wildcard = true;
libc_negatable.had_unrecognized_values = false;
} else if libc_negatable.had_unrecognized_values
&& *libc_str != *b"any"
&& *libc_str != *b"none"
{
// Only error for truly unrecognized values (not "any" or "none")
Output::err_generic(
"Invalid libc: '{}'. Valid values are: *, any, glibc, musl. Use !name to negate.",
(bstr::BStr::new(libc_str),),
);
Global::crash();
}
}
cli.libc = libc_negatable.combine();
}

if matches!(subcommand, Subcommand::Add | Subcommand::Install) {
cli.development = args.flag(b"--development") || args.flag(b"--dev");
cli.optional = args.flag(b"--optional");
Expand Down
2 changes: 1 addition & 1 deletion src/install/PackageManager/PackageManagerLifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ impl PackageManager {
PreinstallState::Unknown => {
// Do not automatically start downloading packages which are disabled
// i.e. don't download all of esbuild's versions or SWCs
if pkg.is_disabled(self.options.cpu, self.options.os) {
if pkg.is_disabled(self.options.cpu, self.options.os, self.options.libc) {
self.set_preinstall_state(pkg.meta.id, PreinstallState::Done);
return PreinstallState::Done;
}
Expand Down
6 changes: 5 additions & 1 deletion src/install/PackageManager/PackageManagerOptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ pub struct Options {
pub cpu: Npm::Architecture,
/// Override OS for optional dependencies filtering
pub os: Npm::OperatingSystem,
/// Override libc for optional dependencies filtering
pub libc: Npm::Libc,

pub config_version: Option<ConfigVersion>,
}
Expand Down Expand Up @@ -156,6 +158,7 @@ impl Default for Options {
minimum_release_age_excludes: None,
cpu: Npm::Architecture::CURRENT,
os: Npm::OperatingSystem::CURRENT,
libc: Npm::Libc::CURRENT,
config_version: None,
}
}
Expand Down Expand Up @@ -801,9 +804,10 @@ impl Options {
.store(backend as u8, core::sync::atomic::Ordering::Relaxed);
}

// CPU and OS are now parsed as enums in CommandLineArguments, just copy them
// CPU, OS, and libc are now parsed as enums in CommandLineArguments, just copy them
self.cpu = cli.cpu;
self.os = cli.os;
self.libc = cli.libc;

self.do_.set(Do::UPDATE_TO_LATEST, cli.latest);
self.do_.set(Do::RECURSIVE, cli.recursive);
Expand Down
6 changes: 5 additions & 1 deletion src/install/lockfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -690,8 +690,9 @@
meta: &package::Meta,
cpu: Npm::Architecture,
os: Npm::OperatingSystem,
libc: Npm::Libc,
) -> bool {
if meta.is_disabled(cpu, os) {
if meta.is_disabled(cpu, os, libc) {
return true;
}

Expand Down Expand Up @@ -1650,6 +1651,9 @@
if pkg_meta.arch == Npm::Architecture::ALL {
pkg_meta.arch = pkg.package.cpu;
}
if pkg_meta.libc == Npm::Libc::NONE {
pkg_meta.libc = pkg.package.libc;
}

Check warning on line 1656 in src/install/lockfile.rs

View check run for this annotation

Claude / Claude Code Review

Remaining os/cpu code paths not updated for libc

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; *
Comment on lines +1654 to +1656

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.

}
}
_ => {}
Expand Down
10 changes: 8 additions & 2 deletions src/install/lockfile/Package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,8 +406,13 @@ impl<SemverIntType: VersionInt> Alphabetizer<SemverIntType> {

impl<SemverIntType: VersionInt> Package<SemverIntType> {
#[inline]
pub fn is_disabled(&self, cpu: Npm::Architecture, os: Npm::OperatingSystem) -> bool {
self.meta.is_disabled(cpu, os)
pub fn is_disabled(
&self,
cpu: Npm::Architecture,
os: Npm::OperatingSystem,
libc: Npm::Libc,
) -> bool {
self.meta.is_disabled(cpu, os, libc)
}
}

Expand Down Expand Up @@ -937,6 +942,7 @@ impl Package<u64> {

package.meta.arch = package_version.cpu;
package.meta.os = package_version.os;
package.meta.libc = package_version.libc;
package.meta.integrity = package_version.integrity;
package
.meta
Expand Down
17 changes: 12 additions & 5 deletions src/install/lockfile/Package/Meta.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use bun_install::integrity::Integrity;
use bun_install::npm::{Architecture, OperatingSystem};
use bun_install::npm::{Architecture, Libc, OperatingSystem};
use bun_install::{INVALID_PACKAGE_ID, Origin, PackageID};
use bun_semver::String;

Expand All @@ -18,7 +18,8 @@ pub struct Meta {

pub arch: Architecture,
pub os: OperatingSystem,
pub _padding_os: u16,
pub libc: Libc,
pub _padding_os: u8,

pub id: PackageID,

Expand Down Expand Up @@ -52,6 +53,7 @@ impl Default for Meta {
_padding_origin: 0,
arch: Architecture::ALL,
os: OperatingSystem::ALL,
libc: Libc::NONE,
_padding_os: 0,
id: INVALID_PACKAGE_ID,
man_dir: String::default(),
Expand All @@ -63,10 +65,14 @@ impl Default for Meta {
}

impl Meta {
/// Does the `cpu` arch and `os` match the requirements listed in the package?
/// Does the `cpu` arch, `os`, and `libc` match the requirements listed in the package?
/// This is completely unrelated to "devDependencies", "peerDependencies", "optionalDependencies" etc
pub fn is_disabled(&self, cpu: Architecture, os: OperatingSystem) -> bool {
!self.arch.is_match(cpu) || !self.os.is_match(os)
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))
}
Comment on lines +70 to 76

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.


pub fn has_install_script(&self) -> bool {
Expand Down Expand Up @@ -111,6 +117,7 @@ impl Meta {
integrity: self.integrity,
arch: self.arch,
os: self.os,
libc: self.libc,
origin: self.origin,
has_install_script: self.has_install_script,
..Meta::default()
Expand Down
6 changes: 5 additions & 1 deletion src/install/lockfile/Tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,11 @@ pub fn is_filtered_dependency_or_workspace(
let res = &pkg_resolutions[pkg_id as usize];
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,
) {
if manager.options.log_level.is_verbose() {
let meta = &pkg_metas[pkg_id as usize];
let name = lockfile.str(&pkg_names[pkg_id as usize]);
Expand Down
26 changes: 13 additions & 13 deletions src/install/lockfile/bun.lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1031,15 +1031,6 @@ impl Stringifier {
writer.write_all(b" \"bundled\": true")?;
}

// TODO(dylan-conway)
// if (meta.libc != .all) {
// try writer.writeAll(
// \\"libc": [
// );
// try Negatable(Npm.Libc).toJson(meta.libc, writer);
// try writer.writeAll("], ");
// }

if meta.os != Npm::OperatingSystem::ALL {
if any {
writer.write_byte(b',')?;
Expand All @@ -1060,6 +1051,16 @@ impl Stringifier {
Negatable::<Npm::Architecture>::to_json(meta.arch, &mut AsFmt::new(writer))?;
}

if meta.libc != Npm::Libc::NONE && meta.libc != Npm::Libc::ALL {
if any {
writer.write_byte(b',')?;
} else {
any = true;
}
writer.write_all(b" \"libc\": ")?;
Negatable::<Npm::Libc>::to_json(meta.libc, &mut AsFmt::new(writer))?;
}

if bin.tag != BinTag::None {
if any {
writer.write_byte(b',')?;
Expand Down Expand Up @@ -2436,10 +2437,9 @@ pub fn parse_into_binary_lockfile(
pkg.meta.arch =
Npm::negatable_from_json::<Npm::Architecture>(&arch)?;
}
// TODO(dylan-conway)
// if (os_cpu_libc_obj.get("libc")) |libc| {
// pkg.meta.libc = Negatable(Npm.Libc).fromJson(allocator, libc);
// }
if let Some(libc) = deps_os_cpu_libc_bin_bundle_obj.get(b"libc") {
pkg.meta.libc = Npm::negatable_from_json::<Npm::Libc>(&libc)?;
}
}
}
ResolutionTag::Root => {
Expand Down
1 change: 1 addition & 0 deletions src/install/lockfile/printer/tree_printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ fn should_print_package_install<'a>(
&pkg_metas[package_id as usize],
this.options.cpu,
this.options.os,
this.options.libc,
) {
return ShouldPrintPackageInstallResult::No;
}
Expand Down
11 changes: 10 additions & 1 deletion src/install_types/resolver_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -916,9 +916,18 @@ impl Libc {

pub const ALL_VALUE: u8 = Self::GLIBC | Self::MUSL;

// TODO: (matches Zig — runtime libc detection)
// 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);
Comment on lines +919 to 924

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.


#[cfg(all(target_os = "linux", target_env = "musl"))]
pub const CURRENT_NAME: &'static str = "musl";
#[cfg(not(all(target_os = "linux", target_env = "musl")))]
pub const CURRENT_NAME: &'static str = "glibc";

#[inline]
pub const fn none() -> Self {
Self::NONE
Expand Down
Loading
Loading