Compare commits

...

3 Commits

Author SHA1 Message Date
Claude Bot
a9ad1d5527 fix(install): use std.once for thread-safe libc detection
Address thread safety review comment: The cached_current variable
could race if Libc.current() is called from multiple threads.

Changed to use std.once for thread-safe one-time initialization:
- cached_current_once ensures detectLibcRuntimeOnce() runs exactly once
- cached_current stores the result and is written during init

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:54:05 +00:00
Claude Bot
04ed2fcfed fix(install): implement runtime libc detection for package filtering
Address review comments on PR #26159:

1. Changed `Libc.current` from compile-time const to runtime function
   `Libc.current()` that detects musl vs glibc at runtime. This handles
   cases where a glibc-compiled Bun binary runs on a musl system (e.g.,
   via gcompat on Alpine Linux).

2. Runtime detection checks for musl's dynamic loader paths:
   - /lib/ld-musl-x86_64.so.1 (x64)
   - /lib/ld-musl-aarch64.so.1 (arm64)
   The result is cached after first detection.

3. Updated all usages of `options.libc` to use `options.getLibc()` which
   resolves the `.none` default to the runtime-detected value.

4. Simplified regression test to focus on CLI interface testing. The
   detailed libc filtering is already tested in bun-install-cpu-os.test.ts
   using a mock registry.

5. Added cleanup of both bun.lock and bun.lockb between test runs.

Fixes #26156

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:29:08 +00:00
Claude Bot
34d1c70d06 fix(install): filter optional dependencies by libc field (#26156)
Implement support for the `libc` field in package.json, which is used by
packages like @rollup/rollup-linux-x64-gnu and @rollup/rollup-linux-x64-musl
to specify which libc variant they require.

Changes:
- Add `libc` field to lockfile `Meta` struct
- Implement compile-time libc detection (glibc vs musl on Linux, .all elsewhere)
- Update `isDisabled()` to check libc in addition to cpu and os
- Add `--libc` CLI flag for manually overriding libc (like `--cpu` and `--os`)
- Add libc serialization/deserialization for bun.lock and pnpm lockfiles
- Parse `libc` field from package.json

The isMatch logic handles backwards compatibility by treating `.none` (unset)
the same as `.all`, so packages without a libc field match any system libc.

Fixes #26156

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:04:11 +00:00
25 changed files with 469 additions and 33 deletions

View File

@@ -289,11 +289,13 @@ pub const PackageInstaller = struct {
.native_binlink => {
const target_cpu = manager.options.cpu;
const target_os = manager.options.os;
const target_libc = manager.options.getLibc();
if (PostinstallOptimizer.getNativeBinlinkReplacementPackageID(
pkg_resolutions_lists[package_id].get(pkg_resolutions_buffer),
pkg_metas,
target_cpu,
target_os,
target_libc,
)) |replacement_pkg_id| {
if (tree_id != 0) {
// TODO: support this optimization in nested node_modules
@@ -1172,6 +1174,7 @@ pub const PackageInstaller = struct {
this.lockfile.packages.items(.meta),
this.manager.options.cpu,
this.manager.options.os,
this.manager.options.getLibc(),
this.current_tree_id,
)) {
if (PackageManager.verbose_install) {
@@ -1366,6 +1369,7 @@ pub const PackageInstaller = struct {
this.lockfile.packages.items(.meta),
this.manager.options.cpu,
this.manager.options.os,
this.manager.options.getLibc(),
this.current_tree_id,
)) {
if (PackageManager.verbose_install) {

View File

@@ -53,6 +53,7 @@ const shared_params = [_]ParamType{
clap.parseParam("--minimum-release-age <NUM> Only install packages published at least N seconds ago (security feature)") catch unreachable,
clap.parseParam("--cpu <STR>... Override CPU architecture for optional dependencies (e.g., x64, arm64, * for all)") catch unreachable,
clap.parseParam("--os <STR>... Override operating system for optional dependencies (e.g., linux, darwin, * for all)") catch unreachable,
clap.parseParam("--libc <STR>... Override libc for optional dependencies (e.g., glibc, musl, * for all)") catch unreachable,
clap.parseParam("-h, --help Print this help menu") catch unreachable,
};
@@ -249,9 +250,10 @@ depth: ?usize = null,
audit_level: ?AuditLevel = null,
audit_ignore_list: []const string = &.{},
// CPU and OS overrides for optional dependencies
// CPU, OS, and libc overrides for optional dependencies
cpu: Npm.Architecture = Npm.Architecture.current,
os: Npm.OperatingSystem = Npm.OperatingSystem.current,
libc: Npm.Libc = .none, // Resolved to Npm.Libc.current() when .none
pub const AuditLevel = enum {
low,
@@ -1017,6 +1019,30 @@ pub fn parse(allocator: std.mem.Allocator, comptime subcommand: Subcommand) !Com
cli.os = os_negatable.combine();
}
// Parse multiple --libc flags and combine them using Negatable
const libc_values = args.options("--libc");
if (libc_values.len > 0) {
var libc_negatable = Npm.Libc.none.negatable();
for (libc_values) |libc_str| {
// apply() already handles "any" as wildcard and negation with !
libc_negatable.apply(libc_str);
// Support * as an alias for "any"
if (strings.eqlComptime(libc_str, "*")) {
libc_negatable.had_wildcard = true;
libc_negatable.had_unrecognized_values = false;
} else if (libc_negatable.had_unrecognized_values and
!strings.eqlComptime(libc_str, "any") and
!strings.eqlComptime(libc_str, "none"))
{
// Only error for truly unrecognized values (not "any" or "none")
Output.errGeneric("Invalid libc: '{s}'. Valid values are: *, any, glibc, musl. Use !name to negate.", .{libc_str});
Global.crash();
}
}
cli.libc = libc_negatable.combine();
}
if (comptime subcommand == .add or subcommand == .install) {
cli.development = args.flag("--development") or args.flag("--dev");
cli.optional = args.flag("--optional");

View File

@@ -83,7 +83,7 @@ pub fn determinePreinstallState(
// Do not automatically start downloading packages which are disabled
// i.e. don't download all of esbuild's versions or SWCs
if (pkg.isDisabled(manager.options.cpu, manager.options.os)) {
if (pkg.isDisabled(manager.options.cpu, manager.options.os, manager.options.getLibc())) {
manager.setPreinstallState(pkg.meta.id, lockfile, .done);
return .done;
}

View File

@@ -87,6 +87,8 @@ minimum_release_age_excludes: ?[]const []const u8 = null,
cpu: Npm.Architecture = Npm.Architecture.current,
/// Override OS for optional dependencies filtering
os: Npm.OperatingSystem = Npm.OperatingSystem.current,
/// Override libc for optional dependencies filtering (musl/glibc on Linux)
libc: Npm.Libc = .none, // Resolved to Npm.Libc.current() when .none
config_version: ?bun.ConfigVersion = null,
@@ -124,6 +126,11 @@ pub fn shouldPrintCommandName(this: *const Options) bool {
return this.log_level != .silent and this.do.summary;
}
/// Returns the effective libc value, resolving .none to the runtime-detected value.
pub fn getLibc(this: *const Options) Npm.Libc {
return if (this.libc == .none) Npm.Libc.current() else this.libc;
}
pub const LogLevel = enum {
default,
verbose,
@@ -608,9 +615,11 @@ pub fn load(
PackageInstall.supported_method = backend;
}
// 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
this.cpu = cli.cpu;
this.os = cli.os;
// Resolve .none to the runtime-detected libc value
this.libc = if (cli.libc == .none) Npm.Libc.current() else cli.libc;
this.do.update_to_latest = cli.latest;
this.do.recursive = cli.recursive;

View File

@@ -939,6 +939,7 @@ pub const Installer = struct {
pkg_metas,
manager.options.cpu,
manager.options.os,
manager.options.getLibc(),
null,
)) {
break :enqueue_lifecycle_scripts;
@@ -1321,11 +1322,13 @@ pub const Installer = struct {
const manager = this.manager;
const target_cpu = manager.options.cpu;
const target_os = manager.options.os;
const target_libc = manager.options.getLibc();
if (PostinstallOptimizer.getNativeBinlinkReplacementPackageID(
pkg_resolutions_lists[pkg_id].get(pkg_resolutions_buffer),
pkg_metas,
target_cpu,
target_os,
target_libc,
)) |replacement_pkg_id| {
for (entry_node_ids, 0..) |new_node_id, new_entry_id| {
if (node_pkg_ids[new_node_id.get()] == replacement_pkg_id) {

View File

@@ -411,8 +411,9 @@ pub fn isResolvedDependencyDisabled(
meta: *const Package.Meta,
cpu: Npm.Architecture,
os: Npm.OperatingSystem,
libc: Npm.Libc,
) bool {
if (meta.isDisabled(cpu, os)) return true;
if (meta.isDisabled(cpu, os, libc)) return true;
const dep = lockfile.buffers.dependencies.items[dep_id];
@@ -1022,13 +1023,16 @@ pub fn fetchNecessaryPackageMetadataAfterYarnOrPnpmMigration(this: *Lockfile, ma
pkg_bin.* = pkg.package.bin.clone(manifest.string_buf, manifest.extern_strings_bin_entries, extern_strings_list.items, extern_strings, @TypeOf(&builder), &builder);
// Update os/cpu metadata if not already set
// Update os/cpu/libc metadata if not already set
if (pkg_meta.os == .all) {
pkg_meta.os = pkg.package.os;
}
if (pkg_meta.arch == .all) {
pkg_meta.arch = pkg.package.cpu;
}
if (pkg_meta.libc == .all) {
pkg_meta.libc = pkg.package.libc;
}
},
else => {},
}

View File

@@ -57,8 +57,8 @@ pub fn Package(comptime SemverIntType: type) type {
pub const workspaces = DependencyGroup{ .prop = "workspaces", .field = "workspaces", .behavior = .{ .workspace = true } };
};
pub inline fn isDisabled(this: *const @This(), cpu: Npm.Architecture, os: Npm.OperatingSystem) bool {
return this.meta.isDisabled(cpu, os);
pub inline fn isDisabled(this: *const @This(), cpu: Npm.Architecture, os: Npm.OperatingSystem, libc: Npm.Libc) bool {
return this.meta.isDisabled(cpu, os, libc);
}
pub const Alphabetizer = struct {
@@ -282,6 +282,7 @@ pub fn Package(comptime SemverIntType: type) type {
package.meta.arch = package_json.arch;
package.meta.os = package_json.os;
package.meta.libc = package_json.libc;
package.dependencies.off = @as(u32, @truncate(dependencies_list.items.len));
package.dependencies.len = total_dependencies_count - @as(u32, @truncate(dependencies.len));
@@ -491,6 +492,7 @@ pub fn Package(comptime SemverIntType: type) type {
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.setHasInstallScript(package_version.has_install_script);

View File

@@ -9,7 +9,9 @@ pub const Meta = extern struct {
arch: Npm.Architecture = .all,
os: Npm.OperatingSystem = .all,
_padding_os: u16 = 0,
/// `"libc"` field in package.json, used to filter optional dependencies by libc type (musl/glibc).
libc: Npm.Libc = .all,
_padding_libc: u8 = 0,
id: PackageID = invalid_package_id,
@@ -30,10 +32,10 @@ pub const Meta = extern struct {
_padding_integrity: [2]u8 = .{0} ** 2,
/// 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 isDisabled(this: *const Meta, cpu: Npm.Architecture, os: Npm.OperatingSystem) bool {
return !this.arch.isMatch(cpu) or !this.os.isMatch(os);
pub fn isDisabled(this: *const Meta, cpu: Npm.Architecture, os: Npm.OperatingSystem, libc: Npm.Libc) bool {
return !this.arch.isMatch(cpu) or !this.os.isMatch(os) or !this.libc.isMatch(libc);
}
pub fn hasInstallScript(this: *const Meta) bool {
@@ -63,6 +65,7 @@ pub const Meta = extern struct {
.integrity = this.integrity,
.arch = this.arch,
.os = this.os,
.libc = this.libc,
.origin = this.origin,
.has_install_script = this.has_install_script,
};

View File

@@ -363,7 +363,8 @@ pub fn isFilteredDependencyOrWorkspace(
const res = &pkg_resolutions[pkg_id];
const parent_res = &pkg_resolutions[parent_pkg_id];
if (pkg_metas[pkg_id].isDisabled(manager.options.cpu, manager.options.os)) {
const target_libc = manager.options.getLibc();
if (pkg_metas[pkg_id].isDisabled(manager.options.cpu, manager.options.os, target_libc)) {
if (manager.options.log_level.isVerbose()) {
const meta = &pkg_metas[pkg_id];
const name = lockfile.str(&pkg_names[pkg_id]);
@@ -373,6 +374,8 @@ pub fn isFilteredDependencyOrWorkspace(
Output.prettyErrorln("<d>Skip installing<r> <b>{s}<r> <d>- os mismatch<r>", .{name});
} else if (!meta.arch.isMatch(manager.options.cpu)) {
Output.prettyErrorln("<d>Skip installing<r> <b>{s}<r> <d>- cpu mismatch<r>", .{name});
} else if (!meta.libc.isMatch(target_libc)) {
Output.prettyErrorln("<d>Skip installing<r> <b>{s}<r> <d>- libc mismatch<r>", .{name});
}
}
return true;

View File

@@ -758,14 +758,15 @@ pub const Stringifier = struct {
);
}
// TODO(dylan-conway)
// if (meta.libc != .all) {
// try writer.writeAll(
// \\"libc": [
// );
// try Negatable(Npm.Libc).toJson(meta.libc, writer);
// try writer.writeAll("], ");
// }
if (meta.libc != .all) {
if (any) {
try writer.writeByte(',');
} else {
any = true;
}
try writer.writeAll(" \"libc\": ");
try Negatable(Npm.Libc).toJson(meta.libc, writer);
}
if (meta.os != .all) {
if (any) {
@@ -1826,10 +1827,9 @@ pub fn parseIntoBinaryLockfile(
if (deps_os_cpu_libc_bin_bundle_obj.get("cpu")) |arch| {
pkg.meta.arch = try Negatable(Npm.Architecture).fromJson(allocator, arch);
}
// TODO(dylan-conway)
// if (os_cpu_libc_obj.get("libc")) |libc| {
// pkg.meta.libc = Negatable(Npm.Libc).fromJson(allocator, libc);
// }
if (deps_os_cpu_libc_bin_bundle_obj.get("libc")) |libc| {
pkg.meta.libc = try Negatable(Npm.Libc).fromJson(allocator, libc);
}
}
},
.root => {

View File

@@ -313,6 +313,18 @@ pub fn jsonStringify(this: *const Lockfile, w: anytype) !void {
}
}
if (@as(u8, @intFromEnum(pkg.meta.libc)) != Npm.Libc.all_value) {
try w.objectField("libc");
try w.beginArray();
defer w.endArray() catch {};
for (Npm.Libc.NameMap.kvs) |kv| {
if (pkg.meta.libc.has(kv.value)) {
try w.write(kv.key);
}
}
}
try w.objectField("integrity");
if (pkg.meta.integrity.tag != .unknown) {
try w.print("\"{f}\"", .{pkg.meta.integrity});

View File

@@ -142,6 +142,7 @@ fn shouldPrintPackageInstall(
&pkg_metas[package_id],
this.options.cpu,
this.options.os,
this.options.getLibc(),
)) {
return .no;
}

View File

@@ -715,6 +715,10 @@ pub const Libc = enum(u8) {
}
pub fn isMatch(this: Libc, target: Libc) bool {
// If the package doesn't specify libc (none), it's compatible with any libc
if (this == .none or this == .all) return true;
// If the target is all (e.g., on non-Linux systems), any package libc matches
if (target == .all) return true;
return (@intFromEnum(this) & @intFromEnum(target)) != 0;
}
@@ -722,8 +726,53 @@ pub const Libc = enum(u8) {
return .{ .added = this, .removed = .none };
}
// TODO:
pub const current: Libc = @intFromEnum(glibc);
/// Returns the current system's libc type.
/// On Linux, this detects musl vs glibc at runtime (thread-safe, cached).
/// On other platforms (macOS, Windows), returns .all since libc is not relevant.
pub fn current() Libc {
// On non-Linux platforms, libc type is not relevant for package filtering
if (!Environment.isLinux) return .all;
// If Bun was compiled for musl, we're definitely on a musl system
if (Environment.isMusl) return @enumFromInt(musl);
// For glibc-compiled binaries, detect at runtime if we're actually on a musl system.
// This handles cases like running a glibc binary on Alpine via compatibility layers.
// Uses std.once for thread-safe one-time initialization.
cached_current_once.call();
return cached_current;
}
/// Cached result of runtime libc detection. Written by detectLibcRuntimeOnce.
var cached_current: Libc = @enumFromInt(glibc);
var cached_current_once = std.once(detectLibcRuntimeOnce);
/// Detects the system's libc at runtime by checking for musl's dynamic loader.
/// Musl systems have /lib/ld-musl-<arch>.so.1 as the dynamic loader.
/// This is called exactly once via std.once for thread safety.
fn detectLibcRuntimeOnce() void {
// Check for musl dynamic loader paths based on architecture
const musl_loader_paths = switch (Environment.arch) {
.x64 => &[_][:0]const u8{
"/lib/ld-musl-x86_64.so.1",
"/lib64/ld-musl-x86_64.so.1",
},
.arm64 => &[_][:0]const u8{
"/lib/ld-musl-aarch64.so.1",
"/lib64/ld-musl-aarch64.so.1",
},
else => &[_][:0]const u8{},
};
for (musl_loader_paths) |path| {
if (bun.sys.access(path, std.posix.F_OK).asErr() == null) {
cached_current = @enumFromInt(musl);
return;
}
}
// Default to glibc if no musl loader found (already initialized)
}
const jsc = bun.jsc;
pub fn jsFunctionLibcIsMatch(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
@@ -737,7 +786,7 @@ pub const Libc = enum(u8) {
if (globalObject.hasException()) return .zero;
}
if (globalObject.hasException()) return .zero;
return jsc.JSValue.jsBoolean(libc.combine().isMatch(current));
return jsc.JSValue.jsBoolean(libc.combine().isMatch(current()));
}
};

View File

@@ -622,10 +622,9 @@ pub fn migratePnpmLockfile(
if (package_obj.get("cpu")) |cpu_expr| {
pkg.meta.arch = try Negatable(Npm.Architecture).fromJson(allocator, cpu_expr);
}
// TODO: libc
// if (package_obj.get("libc")) |libc_expr| {
// pkg.meta.libc = try Negatable(Npm.Libc).fromJson(allocator, libc_expr);
// }
if (package_obj.get("libc")) |libc_expr| {
pkg.meta.libc = try Negatable(Npm.Libc).fromJson(allocator, libc_expr);
}
const off, const len = try parseAppendPackageDependencies(
lockfile,

View File

@@ -50,6 +50,7 @@ pub const PostinstallOptimizer = enum {
metas: []const Meta,
target_cpu: Npm.Architecture,
target_os: Npm.OperatingSystem,
target_libc: Npm.Libc,
) ?PackageID {
// Windows needs file extensions.
if (target_os.isMatch(@enumFromInt(Npm.OperatingSystem.win32))) {
@@ -62,7 +63,7 @@ pub const PostinstallOptimizer = enum {
if (resolution >= metas.len) continue;
const meta: *const Meta = &metas[resolution];
if (meta.arch == .all or meta.os == .all) continue;
if (meta.arch.isMatch(target_cpu) and meta.os.isMatch(target_os)) {
if (meta.arch.isMatch(target_cpu) and meta.os.isMatch(target_os) and meta.libc.isMatch(target_libc)) {
return resolution;
}
}
@@ -104,6 +105,7 @@ pub const PostinstallOptimizer = enum {
metas: []const Meta,
target_cpu: Npm.Architecture,
target_os: Npm.OperatingSystem,
target_libc: Npm.Libc,
tree_id: ?Lockfile.Tree.Id,
) bool {
if (bun.env_var.feature_flag.BUN_FEATURE_FLAG_DISABLE_IGNORE_SCRIPTS.get()) {
@@ -123,7 +125,7 @@ pub const PostinstallOptimizer = enum {
// breaking the code.
//
// This shows up in test/integration/esbuild/esbuild.test.ts
getNativeBinlinkReplacementPackageID(resolutions, metas, target_cpu, target_os) != null,
getNativeBinlinkReplacementPackageID(resolutions, metas, target_cpu, target_os, target_libc) != null,
.ignore => true,
};

View File

@@ -64,6 +64,7 @@ pub const PackageJSON = struct {
arch: Architecture = Architecture.all,
os: OperatingSystem = OperatingSystem.all,
libc: Libc = Libc.all,
package_manager_package_id: Install.PackageID = Install.invalid_package_id,
dependencies: DependencyMap = .{},
@@ -963,6 +964,20 @@ pub const PackageJSON = struct {
}
}
if (json.get("libc")) |libc_field| {
var tmp = libc_field.asArray();
if (tmp) |*array| {
var libc = Libc.none.negatable();
while (array.next()) |item| {
if (item.asString(bun.default_allocator)) |str| {
libc.apply(str);
}
}
package_json.libc = libc.combine();
}
}
const DependencyGroup = Install.Lockfile.Package.DependencyGroup;
const features = .{
.dependencies = true,
@@ -2149,6 +2164,7 @@ const resolver = @import("./resolver.zig");
const std = @import("std");
const Architecture = @import("../install/npm.zig").Architecture;
const Libc = @import("../install/npm.zig").Libc;
const OperatingSystem = @import("../install/npm.zig").OperatingSystem;
const bun = @import("bun");

View File

@@ -604,4 +604,181 @@ describe("bun install --cpu and --os flags", () => {
// Should skip x64 dep and install other CPU deps
expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "dep-arm64", "dep-ppc64"]);
});
it("should filter dependencies by libc type", async () => {
const urls: string[] = [];
setHandler(
dummyRegistry(urls, {
"1.0.0": {
libc: ["glibc"],
},
"2.0.0": {
libc: ["musl"],
},
}),
);
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "test-libc-filter",
version: "1.0.0",
optionalDependencies: {
"dep-glibc-only": "1.0.0",
"dep-musl-only": "2.0.0",
},
}),
);
// Install with glibc - should skip the musl-only dependency
const { exited } = spawn({
cmd: [bunExe(), "install", "--libc", "glibc"],
cwd: package_dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await exited;
expect(exitCode).toBe(0);
// The glibc package should be installed, musl should be skipped
expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "dep-glibc-only"]);
// Install with musl - should skip the glibc-only dependency
await rm(join(package_dir, "node_modules"), { recursive: true, force: true });
await rm(join(package_dir, "bun.lockb"), { force: true });
await rm(join(package_dir, "bun.lock"), { force: true });
const { exited: exited2 } = spawn({
cmd: [bunExe(), "install", "--libc", "musl"],
cwd: package_dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode2 = await exited2;
expect(exitCode2).toBe(0);
// The musl package should be installed, glibc should be skipped
expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "dep-musl-only"]);
});
it("should filter dependencies by combined cpu, os and libc", async () => {
const urls: string[] = [];
setHandler(
dummyRegistry(urls, {
"1.0.0": {
cpu: ["x64"],
os: ["linux"],
libc: ["glibc"],
},
"2.0.0": {
cpu: ["x64"],
os: ["linux"],
libc: ["musl"],
},
"3.0.0": {
cpu: ["arm64"],
os: ["linux"],
libc: ["glibc"],
},
}),
);
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "test-combined-filter",
version: "1.0.0",
optionalDependencies: {
"dep-x64-linux-glibc": "1.0.0",
"dep-x64-linux-musl": "2.0.0",
"dep-arm64-linux-glibc": "3.0.0",
},
}),
);
// Install with x64, linux, glibc - should only install the matching package
const { exited } = spawn({
cmd: [bunExe(), "install", "--cpu", "x64", "--os", "linux", "--libc", "glibc"],
cwd: package_dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await exited;
expect(exitCode).toBe(0);
expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "dep-x64-linux-glibc"]);
});
it("should error on invalid libc", async () => {
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "test-invalid-libc",
version: "1.0.0",
dependencies: {},
}),
);
const { stderr, exited } = spawn({
cmd: [bunExe(), "install", "--libc", "invalid-libc"],
cwd: package_dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await exited;
const stderrText = await stderr.text();
expect(exitCode).toBe(1);
expect(stderrText).toContain("Invalid libc");
expect(stderrText).toContain("invalid-libc");
});
it("should support * wildcard for libc", async () => {
const urls: string[] = [];
setHandler(
dummyRegistry(urls, {
"1.0.0": {
libc: ["glibc"],
},
"2.0.0": {
libc: ["musl"],
},
}),
);
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "test-wildcard-libc",
version: "1.0.0",
optionalDependencies: {
"dep-glibc": "1.0.0",
"dep-musl": "2.0.0",
},
}),
);
// Install with * wildcard - should install all packages regardless of libc
const { exited } = spawn({
cmd: [bunExe(), "install", "--libc", "*"],
cwd: package_dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await exited;
expect(exitCode).toBe(0);
// Should install all packages
expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "dep-glibc", "dep-musl"]);
});
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,126 @@
import { expect, it } from "bun:test";
import { rm } from "fs/promises";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
/**
* Regression test for https://github.com/oven-sh/bun/issues/26156
*
* This test verifies that packages with `libc` constraints in their package.json
* are correctly filtered during installation based on the target libc (glibc vs musl).
*
* The issue was that Bun ignored the `libc` field when filtering optional dependencies,
* causing both glibc and musl variants to be installed instead of just the matching one.
*
* Note: The detailed filtering logic is tested in bun-install-cpu-os.test.ts which uses
* a mock registry to test the actual filtering behavior. This test focuses on the CLI
* interface and error handling.
*/
/**
* Helper to clean up lockfiles between test runs
*/
async function cleanupLockfiles(dirPath: string) {
await rm(join(dirPath, "node_modules"), { recursive: true, force: true });
await rm(join(dirPath, "bun.lock"), { force: true });
await rm(join(dirPath, "bun.lockb"), { force: true });
}
it("should filter optional dependencies by libc field (issue #26156)", async () => {
// Create a temporary directory for this test
using dir = tempDir("issue-26156", {
"package.json": JSON.stringify({
name: "test-libc-filtering",
version: "1.0.0",
}),
});
// Verify that --libc flag is recognized and works
await using proc = Bun.spawn({
cmd: [bunExe(), "install", "--help"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// The --libc flag should be documented in help
expect(stdout).toContain("--libc");
expect(exitCode).toBe(0);
});
it("should accept valid libc values", async () => {
using dir = tempDir("issue-26156-valid", {
"package.json": JSON.stringify({
name: "test-libc-valid",
version: "1.0.0",
}),
});
const dirPath = String(dir);
// Test that explicit --libc glibc flag is accepted
await using proc1 = Bun.spawn({
cmd: [bunExe(), "install", "--libc", "glibc"],
env: bunEnv,
cwd: dirPath,
stdout: "pipe",
stderr: "pipe",
});
const exitCode1 = await proc1.exited;
expect(exitCode1).toBe(0);
// Test that explicit --libc musl flag is accepted
await cleanupLockfiles(dirPath);
await using proc2 = Bun.spawn({
cmd: [bunExe(), "install", "--libc", "musl"],
env: bunEnv,
cwd: dirPath,
stdout: "pipe",
stderr: "pipe",
});
const exitCode2 = await proc2.exited;
expect(exitCode2).toBe(0);
// Test that --libc * (wildcard) is accepted
await cleanupLockfiles(dirPath);
await using proc3 = Bun.spawn({
cmd: [bunExe(), "install", "--libc", "*"],
env: bunEnv,
cwd: dirPath,
stdout: "pipe",
stderr: "pipe",
});
const exitCode3 = await proc3.exited;
expect(exitCode3).toBe(0);
});
it("should reject invalid libc values", async () => {
using dir = tempDir("issue-26156-invalid", {
"package.json": JSON.stringify({
name: "test-invalid-libc",
version: "1.0.0",
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "install", "--libc", "invalid-libc-value"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stderr).toContain("Invalid libc");
expect(stderr).toContain("invalid-libc-value");
});