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
4 changes: 4 additions & 0 deletions src/install/PackageInstaller.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
28 changes: 27 additions & 1 deletion src/install/PackageManager/CommandLineArguments.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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");
Expand Down
2 changes: 1 addition & 1 deletion src/install/PackageManager/PackageManagerLifecycle.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
11 changes: 10 additions & 1 deletion src/install/PackageManager/PackageManagerOptions.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/install/isolated_install/Installer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,7 @@ pub const Installer = struct {
pkg_metas,
manager.options.cpu,
manager.options.os,
manager.options.getLibc(),
null,
)) {
break :enqueue_lifecycle_scripts;
Expand Down Expand Up @@ -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) {
Expand Down
8 changes: 6 additions & 2 deletions src/install/lockfile.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down Expand Up @@ -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 => {},
}
Expand Down
6 changes: 4 additions & 2 deletions src/install/lockfile/Package.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);

Expand Down
11 changes: 7 additions & 4 deletions src/install/lockfile/Package/Meta.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand All @@ -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 {
Expand Down Expand Up @@ -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,
};
Expand Down
5 changes: 4 additions & 1 deletion src/install/lockfile/Tree.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand All @@ -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;
Expand Down
24 changes: 12 additions & 12 deletions src/install/lockfile/bun.lock.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 => {
Expand Down
12 changes: 12 additions & 0 deletions src/install/lockfile/lockfile_json_stringify_for_debugging.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand Down
1 change: 1 addition & 0 deletions src/install/lockfile/printer/tree_printer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ fn shouldPrintPackageInstall(
&pkg_metas[package_id],
this.options.cpu,
this.options.os,
this.options.getLibc(),
)) {
return .no;
}
Expand Down
55 changes: 52 additions & 3 deletions src/install/npm.zig
Original file line number Diff line number Diff line change
Expand Up @@ -715,15 +715,64 @@ 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;
}

pub fn negatable(this: Libc) Negatable(Libc) {
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)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

const jsc = bun.jsc;
pub fn jsFunctionLibcIsMatch(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
Expand All @@ -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()));
}
};

Expand Down
Loading
Loading