Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
6286c0392a Add support for --no-bin-links flag
This commit implements the --no-bin-links CLI flag for bun install,
which prevents the creation of symlinks in node_modules/.bin directory.
This is useful for file systems that don't support symbolic links,
such as FAT32 and exFAT.

Changes:
- Added --no-bin-links flag to CommandLineArguments shared_params
- Added link_bins boolean to PackageManagerOptions.Do struct
- Updated PackageInstaller to conditionally skip bin linking
- Updated hoisted_install to respect link_bins option
- Added regression test for the functionality

Fixes #24628

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-12 06:59:30 +00:00
5 changed files with 104 additions and 3 deletions

View File

@@ -230,7 +230,7 @@ pub const PackageInstaller = struct {
this.completed_trees.set(tree_id);
if (tree.binaries.count() > 0) {
if (tree.binaries.count() > 0 and this.options.do.link_bins) {
this.seen_bin_links.clearRetainingCapacity();
var link_target_buf: bun.PathBuffer = undefined;

View File

@@ -39,6 +39,7 @@ const shared_params = [_]ParamType{
clap.parseParam("--no-summary Don't print a summary") catch unreachable,
clap.parseParam("--no-verify Skip verifying integrity of newly downloaded packages") catch unreachable,
clap.parseParam("--ignore-scripts Skip lifecycle scripts in the project's package.json (dependency scripts are never run)") catch unreachable,
clap.parseParam("--no-bin-links Don't create symlinks in node_modules/.bin") catch unreachable,
clap.parseParam("--trust Add to trustedDependencies in the project's package.json and install the package(s)") catch unreachable,
clap.parseParam("-g, --global Install globally") catch unreachable,
clap.parseParam("--cwd <STR> Set a specific cwd") catch unreachable,
@@ -194,6 +195,7 @@ verbose: bool = false,
no_progress: bool = false,
no_verify: bool = false,
ignore_scripts: bool = false,
no_bin_links: bool = false,
trusted: bool = false,
no_summary: bool = false,
latest: bool = false,
@@ -805,6 +807,7 @@ pub fn parse(allocator: std.mem.Allocator, comptime subcommand: Subcommand) !Com
cli.quiet = args.flag("--quiet");
cli.verbose = args.flag("--verbose") or Output.is_verbose;
cli.ignore_scripts = args.flag("--ignore-scripts");
cli.no_bin_links = args.flag("--no-bin-links");
cli.trusted = args.flag("--trust");
cli.no_summary = args.flag("--no-summary");
cli.ca = args.options("--ca");

View File

@@ -558,6 +558,10 @@ pub fn load(
this.do.run_scripts = false;
}
if (cli.no_bin_links) {
this.do.link_bins = false;
}
if (cli.trusted) {
this.do.trust_dependencies_from_args = true;
}
@@ -715,7 +719,8 @@ pub const Do = packed struct(u16) {
analyze: bool = false,
recursive: bool = false,
prefetch_resolved_tarballs: bool = true,
_: u2 = 0,
link_bins: bool = true,
_: u1 = 0,
};
pub const Enable = packed struct(u16) {

View File

@@ -341,7 +341,9 @@ pub fn installHoistedPackages(
// need to make sure bins are linked before completing any remaining scripts.
// this can happen if a package fails to download
installer.linkRemainingBins(log_level);
if (installer.options.do.link_bins) {
installer.linkRemainingBins(log_level);
}
installer.completeRemainingScripts(log_level);
// .monotonic is okay because this value is only accessed on this thread.

View File

@@ -0,0 +1,91 @@
import { expect, setDefaultTimeout, test } from "bun:test";
import { existsSync, readdirSync } from "fs";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
setDefaultTimeout(60000);
test("--no-bin-links prevents creation of symlinks in node_modules/.bin", async () => {
// https://github.com/oven-sh/bun/issues/24628
// Create temp directory with test package that has a bin
using dir = tempDir("no-bin-links-test", {
"package.json": JSON.stringify({
name: "test-app",
version: "1.0.0",
dependencies: {
typescript: "5.7.2",
},
}),
});
// Run bun install with --no-bin-links flag
await using proc = Bun.spawn({
cmd: [bunExe(), "install", "--no-bin-links"],
env: bunEnv,
cwd: String(dir),
stderr: "inherit",
stdout: "inherit",
});
const exitCode = await proc.exited;
// Installation should succeed
expect(exitCode).toBe(0);
// Check that typescript was installed
const typescriptDir = join(String(dir), "node_modules", "typescript");
expect(existsSync(typescriptDir)).toBe(true);
// Check that node_modules/.bin does NOT exist or is empty
const binDir = join(String(dir), "node_modules", ".bin");
const binDirExists = existsSync(binDir);
if (binDirExists) {
// If .bin exists, it should be empty or not contain typescript bins
const binContents = readdirSync(binDir);
expect(binContents).not.toContain("tsc");
expect(binContents).not.toContain("tsserver");
} else {
// .bin directory should not exist at all
expect(binDirExists).toBe(false);
}
});
test("without --no-bin-links, bin links are created normally", async () => {
// Create temp directory with test package that has a bin
using dir = tempDir("with-bin-links-test", {
"package.json": JSON.stringify({
name: "test-app",
version: "1.0.0",
dependencies: {
typescript: "5.7.2",
},
}),
});
// Run bun install WITHOUT --no-bin-links flag
await using proc = Bun.spawn({
cmd: [bunExe(), "install"],
env: bunEnv,
cwd: String(dir),
stderr: "inherit",
stdout: "inherit",
});
const exitCode = await proc.exited;
// Installation should succeed
expect(exitCode).toBe(0);
// Check that typescript was installed
const typescriptDir = join(String(dir), "node_modules", "typescript");
expect(existsSync(typescriptDir)).toBe(true);
// Check that node_modules/.bin exists and contains the bins
const binDir = join(String(dir), "node_modules", ".bin");
expect(existsSync(binDir)).toBe(true);
const binContents = readdirSync(binDir);
expect(binContents).toContain("tsc");
expect(binContents).toContain("tsserver");
});