Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
10e8075
manual install flags + basic level of libc filtering
RiskyMH Jul 18, 2025
c5d2928
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 18, 2025
22f707b
fix tests
RiskyMH Jul 18, 2025
1df8ad0
.
RiskyMH Jul 18, 2025
c31bd95
feedback
RiskyMH Jul 22, 2025
5cacd0d
Update npm.zig
RiskyMH Jul 22, 2025
e76f90f
Merge branch 'main' into riskymh/install-libc
RiskyMH Jul 22, 2025
48f0a7f
.
RiskyMH Jul 22, 2025
9d6b3d0
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 22, 2025
5b56b6e
better tests
RiskyMH Jul 24, 2025
b73c906
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 24, 2025
07f1beb
Merge branch 'main' into riskymh/install-libc
RiskyMH Jul 24, 2025
f3879a5
test more
RiskyMH Jul 24, 2025
6703de0
more tests
RiskyMH Jul 24, 2025
9db6309
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 24, 2025
8a78796
more feedback fixes
RiskyMH Jul 24, 2025
5f95d87
Update Tree.zig
RiskyMH Jul 24, 2025
0a2aea0
Update bun.lock.zig
RiskyMH Jul 24, 2025
8e6d4e4
Update package_json.zig
RiskyMH Jul 24, 2025
c5eb9c1
Update package_json.zig
RiskyMH Jul 24, 2025
4be5a57
.
RiskyMH Jul 24, 2025
a39255d
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 24, 2025
bfcc678
.
RiskyMH Jul 24, 2025
a8a7225
simplify
RiskyMH Jul 24, 2025
2db492e
oops
RiskyMH Jul 24, 2025
1445e86
.
RiskyMH Jul 24, 2025
b1914a6
.
RiskyMH Jul 24, 2025
56c7667
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 24, 2025
ad5f00e
feedback
RiskyMH Jul 29, 2025
7b78979
nvm
RiskyMH Jul 29, 2025
006b221
Update npm.zig
RiskyMH Jul 29, 2025
0037bbe
Merge branch 'main' into riskymh/install-libc
RiskyMH Jul 29, 2025
ec2f2aa
an actual solution?
RiskyMH Jul 29, 2025
afbeae1
Merge branch 'main' into riskymh/install-libc
RiskyMH Jul 30, 2025
9d9ccbc
Update yarn.zig
RiskyMH Jul 30, 2025
0110d33
Update yarn.zig
RiskyMH Jul 30, 2025
86ca965
Update yarn.zig
RiskyMH Jul 30, 2025
414f08a
Merge branch 'main' into riskymh/install-libc
RiskyMH Aug 2, 2025
dd67609
not used anymore
RiskyMH Aug 2, 2025
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
12 changes: 12 additions & 0 deletions docs/cli/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,18 @@ $ bun install --omit dev
$ bun install --omit=dev --omit=peer --omit=optional
```

## Platform-specific installation

To add packages for a specific platform and omit downloading the rest:

```bash
$ bun install --os=win32 # i.e @oven/bun-windows-x64
$ bun install --os=darwin --cpu=arm64 # i.e @oven/bun-darwin-aarch64
$ bun install --os=linux --cpu=x64 --libc=glibc # i.e @oven/bun-linux-x64
$ bun install --os=linux --cpu=arm64 --libc=musl # i.e @oven/bun-linux-aarch64-musl
$ bun install --os=* --cpu=* --libc=* # <everything>
```

## Dry run

To perform a dry run (i.e. don't actually install anything):
Expand Down
3 changes: 2 additions & 1 deletion packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7989,7 +7989,7 @@ declare module "bun" {

/**
* ```
* INFO = { prod/dev/optional/peer dependencies, os, cpu, libc (TODO), bin, binDir }
* INFO = { prod/dev/optional/peer dependencies, os, cpu, libc, bin, binDir }
*
* // first index is resolution for each type of package
* npm -> [ "name@version", registry (TODO: remove if default), INFO, integrity]
Expand Down Expand Up @@ -8025,6 +8025,7 @@ declare module "bun" {
type BunLockFilePackageInfo = BunLockFileBasePackageInfo & {
os?: string | string[];
cpu?: string | string[];
libc?: string | string[];
bundled?: true;
};

Expand Down
43 changes: 39 additions & 4 deletions src/install/NetworkTask.zig
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ pub const Authorization = enum {
// https://github.com/oven-sh/bun/issues/341
// https://www.jfrog.com/jira/browse/RTFACT-18398
const accept_header_value = "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*";
const accept_header_value_unoptimised = "application/json; q=1.0, */*";

const default_headers_buf: string = "Accept" ++ accept_header_value;
const default_headers_buf_unoptimised: string = "Accept" ++ accept_header_value_unoptimised;

fn appendAuth(header_builder: *HeaderBuilder, scope: *const Npm.Registry.Scope) void {
if (scope.token.len > 0) {
Expand All @@ -70,6 +72,35 @@ fn countAuth(header_builder: *HeaderBuilder, scope: *const Npm.Registry.Scope) v
header_builder.count("npm-auth-type", "legacy");
}

// if this package is likely to have a "libc" field in its package.json.
// npm's optimised response doesn't include it, so we need to use the unoptimised one
fn isPlatformSpecificPackage(name: string) bool {
if (name.len < 5) return false;
const platform_keywords = [_][]const u8{
"musl", "linux", "x64", "aarch64", "glibc", "arm64",
};

if (name[0] == '@') {
if (strings.indexOfChar(name, '/')) |i| {
const scope_and_pkg = name[i + 1 ..];
inline for (platform_keywords) |keyword| {
if (strings.containsComptime(scope_and_pkg, keyword)) {
return true;
}
}
}
} else if (strings.indexOfChar(name, '-')) |i| {
Comment on lines +83 to +92
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (name[0] == '@') {
if (strings.indexOfChar(name, '/')) |i| {
const scope_and_pkg = name[i + 1 ..];
inline for (platform_keywords) |keyword| {
if (strings.containsComptime(scope_and_pkg, keyword)) {
return true;
}
}
}
} else if (strings.indexOfChar(name, '-')) |i| {
if (strings.indexOfChar(name, '/')) |i| {
const scope_and_pkg = name[i + 1 ..];
inline for (platform_keywords) |keyword| {
if (strings.containsComptime(scope_and_pkg, keyword)) {
return true;
}
}
} else if (strings.indexOfChar(name, '-')) |i| {

just makes it a little more simple. we can assume the package name is valid. also, when the package has a '/', should we also check for '-' before looking for keywords?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was half trying to have best perf as who knows how many times this fn gets ran (yes i know its not too much, but every little thing right)

Yeah we could check for - there too but im not sure if its going to gain much and I haven't checked enough to see whats in use (ie is there a @mybuild/win32 or smth)

Copy link
Member

@dylan-conway dylan-conway Jul 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mybuild/win32 is a good example, wouldn't be surprised if this is common. let's not check for '-' if it's scoped

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think(/hope) anyone puts the specific stuff in the org part, so I think its fine to only search the actual name part

In anycase, this should yield a default true for unmatched packages in this rudimentary search (as if bun doesn't see libc, it assumes all)

const pkg_name = name[i + 1 ..];
inline for (platform_keywords) |keyword| {
if (strings.containsComptime(pkg_name, keyword)) {
return true;
}
}
}

return false;
}

pub fn forManifest(
this: *NetworkTask,
name: string,
Expand Down Expand Up @@ -163,8 +194,12 @@ pub fn forManifest(
header_builder.count("If-Modified-Since", last_modified);
}

const use_unoptimized = is_optional and isPlatformSpecificPackage(name);
const accept_value = if (use_unoptimized) accept_header_value_unoptimised else accept_header_value;
const accept_buf = if (use_unoptimized) default_headers_buf_unoptimised else default_headers_buf;

if (header_builder.header_count > 0) {
header_builder.count("Accept", accept_header_value);
header_builder.count("Accept", accept_value);
if (last_modified.len > 0 and etag.len > 0) {
header_builder.content.count(last_modified);
}
Expand All @@ -178,7 +213,7 @@ pub fn forManifest(
header_builder.append("If-Modified-Since", last_modified);
}

header_builder.append("Accept", accept_header_value);
header_builder.append("Accept", accept_value);

if (last_modified.len > 0 and etag.len > 0) {
last_modified = header_builder.content.append(last_modified);
Expand All @@ -188,11 +223,11 @@ pub fn forManifest(
allocator,
.{
.name = .{ .offset = 0, .length = @as(u32, @truncate("Accept".len)) },
.value = .{ .offset = "Accept".len, .length = @as(u32, @truncate(default_headers_buf.len - "Accept".len)) },
.value = .{ .offset = "Accept".len, .length = @as(u32, @truncate(accept_buf.len - "Accept".len)) },
},
);
header_builder.header_count = 1;
header_builder.content = GlobalStringBuilder{ .ptr = @as([*]u8, @ptrFromInt(@intFromPtr(bun.span(default_headers_buf).ptr))), .len = default_headers_buf.len, .cap = default_headers_buf.len };
header_builder.content = GlobalStringBuilder{ .ptr = @as([*]u8, @ptrFromInt(@intFromPtr(bun.span(accept_buf).ptr))), .len = accept_buf.len, .cap = accept_buf.len };
}

this.response_buffer = try MutableString.init(allocator, 0);
Expand Down
55 changes: 40 additions & 15 deletions src/install/PackageManager/CommandLineArguments.zig
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,25 @@ 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("--os <STR> Override OS of native modules to install (e.g., linux, darwin, win32, *)") catch unreachable,
clap.parseParam("--cpu <STR> Override CPU architecture of native modules to install (e.g., x64, arm64, *)") catch unreachable,
clap.parseParam("--libc <STR> Override libc of native modules to install (e.g., glibc, musl, *)") catch unreachable,
clap.parseParam("-h, --help Print this help menu") catch unreachable,
};

pub const install_params: []const ParamType = &(shared_params ++ [_]ParamType{
clap.parseParam("-d, --dev Add dependency to \"devDependencies\"") catch unreachable,
clap.parseParam("-d, --dev Add dependency to \"devDependencies\"") catch unreachable,
clap.parseParam("-D, --development") catch unreachable,
clap.parseParam("--optional Add dependency to \"optionalDependencies\"") catch unreachable,
clap.parseParam("--peer Add dependency to \"peerDependencies\"") catch unreachable,
clap.parseParam("-E, --exact Add the exact version instead of the ^range") catch unreachable,
clap.parseParam("--filter <STR>... Install packages for the matching workspaces") catch unreachable,
clap.parseParam("-a, --analyze Analyze & install all dependencies of files passed as arguments recursively (using Bun's bundler)") catch unreachable,
clap.parseParam("--only-missing Only add dependencies to package.json if they are not already present") catch unreachable,
clap.parseParam("<POS> ... ") catch unreachable,
clap.parseParam("-O, --optional Add dependency to \"optionalDependencies\"") catch unreachable,
clap.parseParam("--peer Add dependency to \"peerDependencies\"") catch unreachable,
clap.parseParam("--save-dev") catch unreachable,
clap.parseParam("--save-optional") catch unreachable,
clap.parseParam("--save-peer") catch unreachable,
clap.parseParam("-E, --exact Add the exact version instead of the ^range") catch unreachable,
clap.parseParam("--filter <STR>... Install packages for the matching workspaces") catch unreachable,
clap.parseParam("-a, --analyze Analyze & install all dependencies of files passed as arguments recursively (using Bun's bundler)") catch unreachable,
clap.parseParam("--only-missing Only add dependencies to package.json if they are not already present") catch unreachable,
clap.parseParam("<POS> ... ") catch unreachable,
});

pub const update_params: []const ParamType = &(shared_params ++ [_]ParamType{
Expand All @@ -88,11 +94,14 @@ pub const pm_params: []const ParamType = &(shared_params ++ [_]ParamType{
});

pub const add_params: []const ParamType = &(shared_params ++ [_]ParamType{
clap.parseParam("-d, --dev Add dependency to \"devDependencies\"") catch unreachable,
clap.parseParam("-d, --dev Add dependency to \"devDependencies\"") catch unreachable,
clap.parseParam("-D, --development") catch unreachable,
clap.parseParam("--optional Add dependency to \"optionalDependencies\"") catch unreachable,
clap.parseParam("--peer Add dependency to \"peerDependencies\"") catch unreachable,
clap.parseParam("-E, --exact Add the exact version instead of the ^range") catch unreachable,
clap.parseParam("-O, --optional Add dependency to \"optionalDependencies\"") catch unreachable,
clap.parseParam("--peer Add dependency to \"peerDependencies\"") catch unreachable,
clap.parseParam("--save-dev") catch unreachable,
clap.parseParam("--save-optional") catch unreachable,
clap.parseParam("--save-peer") catch unreachable,
clap.parseParam("-E, --exact Add the exact version instead of the ^range") catch unreachable,
clap.parseParam("-a, --analyze Recursively analyze & install dependencies of files passed as arguments (using Bun's bundler)") catch unreachable,
clap.parseParam("--only-missing Only add dependencies to package.json if they are not already present") catch unreachable,
clap.parseParam("<POS> ... \"name\" or \"name@version\" of package(s) to install") catch unreachable,
Expand Down Expand Up @@ -220,6 +229,10 @@ lockfile_only: bool = false,

node_linker: ?Options.NodeLinker = null,

target_os: ?string = null,
target_cpu: ?string = null,
target_libc: ?string = null,

// `bun pm version` options
git_tag_version: bool = true,
allow_same_version: bool = false,
Expand Down Expand Up @@ -735,6 +748,18 @@ pub fn parse(allocator: std.mem.Allocator, comptime subcommand: Subcommand) !Com
cli.node_linker = .fromStr(linker);
}

if (args.option("--os")) |os| {
cli.target_os = os;
}

if (args.option("--cpu")) |cpu| {
cli.target_cpu = cpu;
}

if (args.option("--libc")) |libc| {
cli.target_libc = libc;
}

if (args.option("--cache-dir")) |cache_dir| {
cli.cache_dir = cache_dir;
}
Expand Down Expand Up @@ -862,9 +887,9 @@ pub fn parse(allocator: std.mem.Allocator, comptime subcommand: Subcommand) !Com
}

if (comptime subcommand == .add or subcommand == .install) {
cli.development = args.flag("--development") or args.flag("--dev");
cli.optional = args.flag("--optional");
cli.peer = args.flag("--peer");
cli.development = args.flag("--development") or args.flag("--dev") or args.flag("--save-dev");
cli.optional = args.flag("--optional") or args.flag("--save-optional");
cli.peer = args.flag("--peer") or args.flag("--save-peer");
cli.exact = args.flag("--exact");
cli.analyze = args.flag("--analyze");
cli.only_missing = args.flag("--only-missing");
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()) {
if (pkg.isDisabled(manager)) {
manager.setPreinstallState(pkg.meta.id, lockfile, .done);
return .done;
}
Expand Down
37 changes: 37 additions & 0 deletions src/install/PackageManager/PackageManagerOptions.zig
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ depth: ?usize = null,
/// isolated installs (pnpm-like) or hoisted installs (yarn-like, original)
node_linker: NodeLinker = .auto,

target_os: ?Npm.OperatingSystem = null,
target_cpu: ?Npm.Architecture = null,
target_libc: ?Npm.Libc = null,

pub const PublishConfig = struct {
access: ?Access = null,
tag: string = "",
Expand Down Expand Up @@ -539,6 +543,39 @@ pub fn load(
this.node_linker = node_linker;
}

if (cli.target_os) |os_str| {
if (strings.eqlComptime(os_str, "*")) {
this.target_os = Npm.OperatingSystem.all;
} else if (Npm.OperatingSystem.NameMap.get(os_str)) |os_value| {
this.target_os = @enumFromInt(os_value);
} else {
Output.errGeneric("invalid --os value: '{s}'. Valid values are: " ++ Npm.OperatingSystem.valid_values_string ++ ", *", .{os_str});
bun.Global.exit(1);
}
}

if (cli.target_cpu) |cpu_str| {
if (strings.eqlComptime(cpu_str, "*")) {
this.target_cpu = Npm.Architecture.all;
} else if (Npm.Architecture.NameMap.get(cpu_str)) |cpu_value| {
this.target_cpu = @enumFromInt(cpu_value);
} else {
Output.errGeneric("invalid --cpu value: '{s}'. Valid values are: " ++ Npm.Architecture.valid_values_string ++ ", *", .{cpu_str});
bun.Global.exit(1);
}
}

if (cli.target_libc) |libc_str| {
if (strings.eqlComptime(libc_str, "*")) {
this.target_libc = Npm.Libc.all;
} else if (Npm.Libc.NameMap.get(libc_str)) |libc_value| {
this.target_libc = @enumFromInt(libc_value);
} else {
Output.errGeneric("invalid --libc value: '{s}'. Valid values are: " ++ Npm.Libc.valid_values_string ++ ", *", .{libc_str});
bun.Global.exit(1);
}
}

const disable_progress_bar = default_disable_progress_bar or cli.no_progress;

if (cli.verbose) {
Expand Down
3 changes: 2 additions & 1 deletion src/install/lockfile.zig
Original file line number Diff line number Diff line change
Expand Up @@ -382,8 +382,9 @@ pub fn isResolvedDependencyDisabled(
dep_id: DependencyID,
features: Features,
meta: *const Package.Meta,
manager: *PackageManager,
) bool {
if (meta.isDisabled()) return true;
if (meta.isDisabled(manager)) return true;

const dep = lockfile.buffers.dependencies.items[dep_id];

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 @@ -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, manager: *const PackageManager) bool {
return this.meta.isDisabled(manager);
}

pub const Alphabetizer = struct {
Expand Down Expand Up @@ -277,6 +277,7 @@ pub const Package = extern struct {

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 @@ -487,6 +488,7 @@ pub const Package = extern struct {

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
15 changes: 11 additions & 4 deletions src/install/lockfile/Package/Meta.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ pub const Meta = extern struct {

arch: Npm.Architecture = .all,
os: Npm.OperatingSystem = .all,
_padding_os: u16 = 0,
libc: Npm.Libc = .none,
_padding_after_platform: u8 = 0,

id: PackageID = invalid_package_id,

Expand All @@ -28,12 +29,16 @@ pub const Meta = extern struct {
true,
} = .false,

_padding_integrity: [2]u8 = .{0} ** 2,
_padding_integrity: u8 = 0,
_padding_end: u8 = 0,

/// 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, manager: *const PackageManager) bool {
const os_match = this.os.isMatch(manager.options.target_os);
const cpu_match = this.arch.isMatch(manager.options.target_cpu);
const libc_match = this.libc.isMatch(manager.options.target_libc);
return !os_match or !cpu_match or !libc_match;
}

pub fn hasInstallScript(this: *const Meta) bool {
Expand Down Expand Up @@ -63,6 +68,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 All @@ -78,4 +84,5 @@ const install = bun.install;
const Npm = install.Npm;
const Origin = install.Origin;
const PackageID = install.PackageID;
const PackageManager = install.PackageManager;
const invalid_package_id = install.invalid_package_id;
24 changes: 17 additions & 7 deletions src/install/lockfile/Tree.zig
Original file line number Diff line number Diff line change
Expand Up @@ -339,17 +339,27 @@ 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)) {
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()) {
Output.prettyErrorln("<d>Skip installing<r> <b>{s}<r> <d>- cpu & os mismatch<r>", .{name});
} else if (!meta.os.isMatch()) {
Output.prettyErrorln("<d>Skip installing<r> <b>{s}<r> <d>- os mismatch<r>", .{name});
} else if (!meta.arch.isMatch()) {
Output.prettyErrorln("<d>Skip installing<r> <b>{s}<r> <d>- cpu mismatch<r>", .{name});
const os_match = meta.os.isMatch(manager.options.target_os);
const arch_match = meta.arch.isMatch(manager.options.target_cpu);
const libc_match = meta.libc.isMatch(manager.options.target_libc);

Output.prettyError("<d>Skip installing<r> <b>{s}<r> <d>- ", .{name});
if (!os_match) {
Output.prettyError("os", .{});
}
if (!arch_match) {
if (!os_match) Output.prettyError(" & ", .{});
Output.prettyError("cpu", .{});
}
if (!libc_match) {
if (!os_match or !arch_match) Output.prettyError(" & ", .{});
Output.prettyError("libc", .{});
}
Output.prettyErrorln(" mismatch<r>", .{});
}
return true;
}
Expand Down
Loading
Loading