mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 06:12:08 +00:00
Compare commits
3 Commits
claude/fix
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9ad1d5527 | ||
|
|
04ed2fcfed | ||
|
|
34d1c70d06 |
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 => {},
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -142,6 +142,7 @@ fn shouldPrintPackageInstall(
|
||||
&pkg_metas[package_id],
|
||||
this.options.cpu,
|
||||
this.options.os,
|
||||
this.options.getLibc(),
|
||||
)) {
|
||||
return .no;
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
BIN
test/cli/install/dep-arm64-linux-glibc-3.0.0.tgz
Normal file
BIN
test/cli/install/dep-arm64-linux-glibc-3.0.0.tgz
Normal file
Binary file not shown.
BIN
test/cli/install/dep-glibc-1.0.0.tgz
Normal file
BIN
test/cli/install/dep-glibc-1.0.0.tgz
Normal file
Binary file not shown.
BIN
test/cli/install/dep-glibc-only-1.0.0.tgz
Normal file
BIN
test/cli/install/dep-glibc-only-1.0.0.tgz
Normal file
Binary file not shown.
BIN
test/cli/install/dep-musl-2.0.0.tgz
Normal file
BIN
test/cli/install/dep-musl-2.0.0.tgz
Normal file
Binary file not shown.
BIN
test/cli/install/dep-musl-only-2.0.0.tgz
Normal file
BIN
test/cli/install/dep-musl-only-2.0.0.tgz
Normal file
Binary file not shown.
BIN
test/cli/install/dep-x64-linux-glibc-1.0.0.tgz
Normal file
BIN
test/cli/install/dep-x64-linux-glibc-1.0.0.tgz
Normal file
Binary file not shown.
BIN
test/cli/install/dep-x64-linux-musl-2.0.0.tgz
Normal file
BIN
test/cli/install/dep-x64-linux-musl-2.0.0.tgz
Normal file
Binary file not shown.
126
test/regression/issue/26156.test.ts
Normal file
126
test/regression/issue/26156.test.ts
Normal 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");
|
||||
});
|
||||
Reference in New Issue
Block a user