Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
7bc0030230 feat: add --cpu and --os flags to bun install for filtering optional dependencies
This commit adds two new CLI flags to 'bun install':
- --cpu <arch>: Override CPU architecture for optional dependency filtering
- --os <platform>: Override operating system for optional dependency filtering

These flags allow developers to install dependencies for different target
platforms, which is useful for:
- CI/CD pipelines building for multiple architectures
- Cross-platform development
- Creating platform-specific bundles

The implementation:
- Updates isDisabled() methods to accept cpu and os parameters
- Passes options through the dependency resolution system
- Validates input values and provides helpful error messages
- Includes comprehensive test coverage

Valid CPU values: arm, arm64, ia32, mips, mipsel, ppc, ppc64, s390, s390x, x32, x64
Valid OS values: aix, darwin, freebsd, linux, openbsd, sunos, win32, android

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-21 06:39:30 +00:00
27 changed files with 388 additions and 10 deletions

View File

@@ -50,6 +50,8 @@ const shared_params = [_]ParamType{
clap.parseParam("--omit <dev|optional|peer>... Exclude 'dev', 'optional', or 'peer' dependencies from install") catch unreachable,
clap.parseParam("--lockfile-only Generate a lockfile without installing dependencies") catch unreachable,
clap.parseParam("--linker <STR> Linker strategy (one of \"isolated\" or \"hoisted\")") catch unreachable,
clap.parseParam("--cpu <STR> Override CPU architecture for optional dependencies (e.g., x64, arm64)") catch unreachable,
clap.parseParam("--os <STR> Override operating system for optional dependencies (e.g., linux, darwin, win32)") catch unreachable,
clap.parseParam("-h, --help Print this help menu") catch unreachable,
};
@@ -241,6 +243,10 @@ depth: ?usize = null,
audit_level: ?AuditLevel = null,
audit_ignore_list: []const string = &.{},
// CPU and OS overrides for optional dependencies
cpu: ?string = null,
os: ?string = null,
pub const AuditLevel = enum {
low,
moderate,
@@ -937,6 +943,14 @@ pub fn parse(allocator: std.mem.Allocator, comptime subcommand: Subcommand) !Com
cli.config = opt;
}
if (args.option("--cpu")) |cpu| {
cli.cpu = cpu;
}
if (args.option("--os")) |os| {
cli.os = os;
}
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()) {
if (pkg.isDisabled(manager.options.cpu, manager.options.os)) {
manager.setPreinstallState(pkg.meta.id, lockfile, .done);
return .done;
}

View File

@@ -74,6 +74,12 @@ node_linker: NodeLinker = .auto,
// Security scanner module path
security_scanner: ?[]const u8 = null,
/// Override CPU architecture for optional dependencies filtering
cpu: Npm.Architecture = Npm.Architecture.current,
/// Override OS for optional dependencies filtering
os: Npm.OperatingSystem = Npm.OperatingSystem.current,
pub const PublishConfig = struct {
access: ?Access = null,
tag: string = "",
@@ -579,6 +585,24 @@ pub fn load(
PackageInstall.supported_method = backend;
}
if (cli.cpu) |cpu_str| {
if (Npm.Architecture.NameMap.get(cpu_str)) |cpu_val| {
this.cpu = @enumFromInt(cpu_val);
} else {
Output.errGeneric("Invalid CPU architecture: '{s}'. Valid values are: arm, arm64, ia32, mips, mipsel, ppc, ppc64, s390, s390x, x32, x64", .{cpu_str});
Global.crash();
}
}
if (cli.os) |os_str| {
if (Npm.OperatingSystem.NameMap.get(os_str)) |os_val| {
this.os = @enumFromInt(os_val);
} else {
Output.errGeneric("Invalid operating system: '{s}'. Valid values are: aix, darwin, freebsd, linux, openbsd, sunos, win32, android", .{os_str});
Global.crash();
}
}
this.do.update_to_latest = cli.latest;
this.do.recursive = cli.recursive;
@@ -709,6 +733,7 @@ const CommandLineArguments = @import("./CommandLineArguments.zig");
const std = @import("std");
const bun = @import("bun");
const Global = bun.Global;
const DotEnv = bun.DotEnv;
const Environment = bun.Environment;
const FD = bun.FD;

View File

@@ -382,8 +382,10 @@ pub fn isResolvedDependencyDisabled(
dep_id: DependencyID,
features: Features,
meta: *const Package.Meta,
cpu: Npm.Architecture,
os: Npm.OperatingSystem,
) bool {
if (meta.isDisabled()) return true;
if (meta.isDisabled(cpu, os)) return true;
const dep = lockfile.buffers.dependencies.items[dep_id];
@@ -2033,6 +2035,7 @@ const ArrayIdentityContext = @import("../identity_context.zig").ArrayIdentityCon
const IdentityContext = @import("../identity_context.zig").IdentityContext;
const bun = @import("bun");
const Npm = @import("./npm.zig");
const Environment = bun.Environment;
const Global = bun.Global;
const GlobalStringBuilder = bun.StringBuilder;

View File

@@ -52,8 +52,8 @@ pub const Package = extern struct {
pub const workspaces = DependencyGroup{ .prop = "workspaces", .field = "workspaces", .behavior = .{ .workspace = true } };
};
pub inline fn isDisabled(this: *const Package) bool {
return this.meta.isDisabled();
pub inline fn isDisabled(this: *const Package, cpu: Npm.Architecture, os: Npm.OperatingSystem) bool {
return this.meta.isDisabled(cpu, os);
}
pub const Alphabetizer = struct {

View File

@@ -32,8 +32,8 @@ pub const Meta = extern struct {
/// Does the `cpu` arch and `os` match the requirements listed in the package?
/// This is completely unrelated to "devDependencies", "peerDependencies", "optionalDependencies" etc
pub fn isDisabled(this: *const Meta) bool {
return !this.arch.isMatch() or !this.os.isMatch();
pub fn isDisabled(this: *const Meta, cpu: Npm.Architecture, os: Npm.OperatingSystem) bool {
return !this.arch.isMatchWithTarget(cpu) or !this.os.isMatchWithTarget(os);
}
pub fn hasInstallScript(this: *const Meta) bool {

View File

@@ -340,15 +340,15 @@ pub fn isFilteredDependencyOrWorkspace(
const res = &pkg_resolutions[pkg_id];
const parent_res = &pkg_resolutions[parent_pkg_id];
if (pkg_metas[pkg_id].isDisabled()) {
if (pkg_metas[pkg_id].isDisabled(manager.options.cpu, manager.options.os)) {
if (manager.options.log_level.isVerbose()) {
const meta = &pkg_metas[pkg_id];
const name = lockfile.str(&pkg_names[pkg_id]);
if (!meta.os.isMatch() and !meta.arch.isMatch()) {
if (!meta.os.isMatchWithTarget(manager.options.os) and !meta.arch.isMatchWithTarget(manager.options.cpu)) {
Output.prettyErrorln("<d>Skip installing<r> <b>{s}<r> <d>- cpu & os mismatch<r>", .{name});
} else if (!meta.os.isMatch()) {
} else if (!meta.os.isMatchWithTarget(manager.options.os)) {
Output.prettyErrorln("<d>Skip installing<r> <b>{s}<r> <d>- os mismatch<r>", .{name});
} else if (!meta.arch.isMatch()) {
} else if (!meta.arch.isMatchWithTarget(manager.options.cpu)) {
Output.prettyErrorln("<d>Skip installing<r> <b>{s}<r> <d>- cpu mismatch<r>", .{name});
}
}

View File

@@ -140,6 +140,8 @@ fn shouldPrintPackageInstall(
dep_id,
this.options.local_package_features,
&pkg_metas[package_id],
this.options.cpu,
this.options.os,
)) {
return .no;
}

View File

@@ -658,6 +658,10 @@ pub const OperatingSystem = enum(u16) {
return (@intFromEnum(this) & @intFromEnum(current)) != 0;
}
pub fn isMatchWithTarget(this: OperatingSystem, target: OperatingSystem) bool {
return (@intFromEnum(this) & @intFromEnum(target)) != 0;
}
pub inline fn has(this: OperatingSystem, other: u16) bool {
return (@intFromEnum(this) & other) != 0;
}
@@ -797,6 +801,10 @@ pub const Architecture = enum(u16) {
return @intFromEnum(this) & @intFromEnum(current) != 0;
}
pub fn isMatchWithTarget(this: Architecture, target: Architecture) bool {
return @intFromEnum(this) & @intFromEnum(target) != 0;
}
pub fn negatable(this: Architecture) Negatable(Architecture) {
return .{ .added = this, .removed = .none };
}

View File

@@ -0,0 +1,326 @@
import { beforeAll, afterAll, beforeEach, afterEach, describe, expect, it } from "bun:test";
import { exists, writeFile, rm } from "fs/promises";
import { join } from "path";
import {
dummyAfterAll,
dummyAfterEach,
dummyBeforeAll,
dummyBeforeEach,
dummyRegistry,
package_dir,
requested,
root_url,
setHandler,
} from "./dummy.registry.js";
import { bunExe, bunEnv, toMatchNodeModulesAt } from "harness";
import { spawn } from "bun";
expect.extend({
toMatchNodeModulesAt,
});
beforeAll(dummyBeforeAll);
afterAll(dummyAfterAll);
beforeEach(dummyBeforeEach);
afterEach(dummyAfterEach);
describe("bun install --cpu and --os flags", () => {
it("should filter dependencies by CPU architecture", async () => {
const urls: string[] = [];
setHandler(
dummyRegistry(urls, {
"1.0.0": {
cpu: ["x64"],
},
}),
);
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "test-cpu-filter",
version: "1.0.0",
dependencies: {
"dep-x64-only": "1.0.0",
},
}),
);
// Install with arm64 CPU - should skip the x64-only dependency
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install", "--cpu", "arm64"],
cwd: package_dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await exited;
expect(exitCode).toBe(0);
// The package should not be installed
const depExists = await exists(join(package_dir, "node_modules", "dep-x64-only"));
expect(depExists).toBe(false);
// Install with x64 CPU - should install the dependency
await rm(join(package_dir, "node_modules"), { recursive: true, force: true });
await rm(join(package_dir, "bun.lockb"), { force: true });
const { exited: exited2 } = spawn({
cmd: [bunExe(), "install", "--cpu", "x64"],
cwd: package_dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode2 = await exited2;
expect(exitCode2).toBe(0);
// The package should be installed
const depExists2 = await exists(join(package_dir, "node_modules", "dep-x64-only"));
expect(depExists2).toBe(true);
});
it("should filter dependencies by OS", async () => {
const urls: string[] = [];
setHandler(
dummyRegistry(urls, {
"1.0.0": {
os: ["linux"],
},
}),
);
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "test-os-filter",
version: "1.0.0",
dependencies: {
"dep-linux-only": "1.0.0",
},
}),
);
// Install with darwin OS - should skip the linux-only dependency
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install", "--os", "darwin"],
cwd: package_dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await exited;
expect(exitCode).toBe(0);
// The package should not be installed
const depExists = await exists(join(package_dir, "node_modules", "dep-linux-only"));
expect(depExists).toBe(false);
// Install with linux OS - should install the dependency
await rm(join(package_dir, "node_modules"), { recursive: true, force: true });
await rm(join(package_dir, "bun.lockb"), { force: true });
const { exited: exited2 } = spawn({
cmd: [bunExe(), "install", "--os", "linux"],
cwd: package_dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode2 = await exited2;
expect(exitCode2).toBe(0);
// The package should be installed
const depExists2 = await exists(join(package_dir, "node_modules", "dep-linux-only"));
expect(depExists2).toBe(true);
});
it("should filter dependencies by both CPU and OS", async () => {
const urls: string[] = [];
setHandler(
dummyRegistry(urls, {
"1.0.0": {
cpu: ["arm64"],
os: ["darwin"],
},
"2.0.0": {
cpu: ["x64"],
os: ["linux"],
},
"3.0.0": {},
}),
);
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "test-cpu-os-filter",
version: "1.0.0",
optionalDependencies: {
"dep-darwin-arm64": "1.0.0",
"dep-linux-x64": "2.0.0",
"dep-universal": "3.0.0",
},
}),
);
// Install with linux/x64 - should only install linux-x64 and universal deps
const { exited } = spawn({
cmd: [bunExe(), "install", "--cpu", "x64", "--os", "linux"],
cwd: package_dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await exited;
expect(exitCode).toBe(0);
// Check which packages were installed
const darwinArm64Exists = await exists(join(package_dir, "node_modules", "dep-darwin-arm64"));
expect(darwinArm64Exists).toBe(false);
const linuxX64Exists = await exists(join(package_dir, "node_modules", "dep-linux-x64"));
expect(linuxX64Exists).toBe(true);
const universalExists = await exists(join(package_dir, "node_modules", "dep-universal"));
expect(universalExists).toBe(true);
});
it("should handle multiple CPU architectures in package metadata", async () => {
const urls: string[] = [];
setHandler(
dummyRegistry(urls, {
"1.0.0": {
cpu: ["x64", "arm64"],
},
}),
);
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "test-multi-cpu",
version: "1.0.0",
dependencies: {
"dep-multi-cpu": "1.0.0",
},
}),
);
// Install with arm64 - should install since arm64 is in the list
const { exited } = spawn({
cmd: [bunExe(), "install", "--cpu", "arm64"],
cwd: package_dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await exited;
expect(exitCode).toBe(0);
const depExists = await exists(join(package_dir, "node_modules", "dep-multi-cpu"));
expect(depExists).toBe(true);
});
it("should error on invalid CPU architecture", async () => {
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "test-invalid-cpu",
version: "1.0.0",
dependencies: {},
}),
);
const { stderr, exited } = spawn({
cmd: [bunExe(), "install", "--cpu", "invalid-cpu"],
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 CPU architecture");
expect(stderrText).toContain("invalid-cpu");
});
it("should error on invalid OS", async () => {
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "test-invalid-os",
version: "1.0.0",
dependencies: {},
}),
);
const { stderr, exited } = spawn({
cmd: [bunExe(), "install", "--os", "invalid-os"],
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 operating system");
expect(stderrText).toContain("invalid-os");
});
it("should skip installing packages with negated CPU/OS", async () => {
const urls: string[] = [];
setHandler(
dummyRegistry(urls, {
"1.0.0": {
cpu: ["!arm64"],
},
"2.0.0": {
os: ["!linux"],
},
}),
);
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "test-negated",
version: "1.0.0",
optionalDependencies: {
"dep-not-arm64": "1.0.0",
"dep-not-linux": "2.0.0",
},
}),
);
// Install with arm64 - should skip dep-not-arm64
const { exited } = spawn({
cmd: [bunExe(), "install", "--cpu", "arm64", "--os", "darwin"],
cwd: package_dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await exited;
expect(exitCode).toBe(0);
const notArm64Exists = await exists(join(package_dir, "node_modules", "dep-not-arm64"));
expect(notArm64Exists).toBe(false);
const notLinuxExists = await exists(join(package_dir, "node_modules", "dep-not-linux"));
expect(notLinuxExists).toBe(true);
});
});

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.

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.

Binary file not shown.

Binary file not shown.

Binary file not shown.