Files
bun.sh/test/cli/install/shebang-normalize.test.ts
robobun 93910f34da Fix bin linking to atomically normalize CRLF in shebang lines (#23360)
## Summary

This PR improves the correctness of bin linking by atomically
normalizing `\r\n` to `\n` in shebang lines when linking bins.

### Changes

- **Refactored shebang normalization in `src/install/bin.zig`**:
  - Extracted logic into separate `tryNormalizeShebang` function
  - Changed from in-place file modification to atomic file replacement
- Reads entire file, creates temporary file with corrected shebang, then
atomically renames
  - Properly cleans up temporary files on errors
  
- **Added test coverage**:
- New test file `test/cli/install/shebang-normalize.test.ts` verifies
CRLF normalization works correctly
- Modified existing test in `bun-link.test.ts` to use Python script with
CRLF shebang

### Why

The previous implementation modified files in-place by seeking to the
`\r` position and overwriting with `\n`. This could potentially corrupt
files if interrupted mid-write. The new atomic approach ensures file
integrity by writing to a temporary file first, then renaming it to
replace the original.

## Test plan

-  `bun bd test test/cli/install/shebang-normalize.test.ts` - passes
-  Verified bins with CRLF shebangs are normalized to LF during linking
-  Code compiles successfully

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

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2025-10-08 01:51:25 -07:00

70 lines
2.0 KiB
TypeScript

import { spawn } from "bun";
import { expect, test } from "bun:test";
import { mkdir, readFile, stat, writeFile } from "fs/promises";
import { bunExe, bunEnv as env, isWindows, runBunInstall, tmpdirSync } from "harness";
import { join } from "path";
test.skipIf(isWindows)("bin linking normalizes CRLF in shebang", async () => {
const testDir = tmpdirSync();
const pkgDir = join(testDir, "pkg");
const consumerDir = join(testDir, "consumer");
await mkdir(pkgDir, { recursive: true });
await mkdir(consumerDir, { recursive: true });
// Create package with bin that has CRLF shebang
await writeFile(
join(pkgDir, "package.json"),
JSON.stringify({
name: "test-pkg-crlf",
version: "1.0.0",
bin: {
"test-bin": "test-bin.py",
},
}),
);
// Write bin file with CRLF shebang
await writeFile(join(pkgDir, "test-bin.py"), "#!/usr/bin/env python\r\nprint('hello from python')");
// Link the package
const linkResult = spawn({
cmd: [bunExe(), "link"],
cwd: pkgDir,
env,
stdout: "pipe",
stderr: "pipe",
});
await linkResult.exited;
expect(linkResult.exitCode).toBe(0);
// Create consumer package
await writeFile(
join(consumerDir, "package.json"),
JSON.stringify({
name: "consumer",
version: "1.0.0",
dependencies: {
"test-pkg-crlf": "link:test-pkg-crlf",
},
}),
);
// Install
await runBunInstall(env, consumerDir);
// Check that the linked bin file has normalized shebang
const binPath = join(consumerDir, "node_modules", "test-pkg-crlf", "test-bin.py");
const binContent = await readFile(binPath, "utf-8");
console.log("Bin content first 50 chars:", JSON.stringify(binContent.slice(0, 50)));
expect(binContent).toStartWith("#!/usr/bin/env python\nprint");
expect(binContent).not.toContain("\r\n");
// Verify that the file is executable (bin linking sets this)
const binStat = await stat(binPath);
expect(binStat.mode & 0o111).toBeGreaterThan(0); // At least one execute bit should be set
});