mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 02:48:50 +00:00
## Summary
This PR introduces a new postinstall optimization system that
significantly reduces the need to run lifecycle scripts for certain
packages by intelligently handling their requirements at install time.
## Key Features
### 1. Native Binlink Optimization
When packages like `esbuild` ship platform-specific binaries as optional
dependencies, we now:
- Detect the native binlink pattern (enabled by default for `esbuild`)
- Find the matching platform-specific dependency based on target CPU/OS
- Link binaries directly from the platform-specific package (e.g.,
`@esbuild/darwin-arm64`)
- Fall back gracefully if the platform-specific package isn't found
**Result**: No postinstall scripts needed for esbuild and similar
packages.
### 2. Lifecycle Script Skipping
For packages like `sharp` that run heavy postinstall scripts:
- Skip lifecycle scripts entirely (enabled by default for `sharp`)
- Prevents downloading large binaries or compiling native code
unnecessarily
- Reduces install time and potential failures in restricted environments
## Configuration
Both features can be configured via `package.json`:
```json
{
"nativeDependencies": ["esbuild", "my-custom-package"],
"ignoreScripts": ["sharp", "another-package"]
}
```
Set to empty arrays to disable defaults:
```json
{
"nativeDependencies": [],
"ignoreScripts": []
}
```
Environment variable overrides:
- `BUN_FEATURE_FLAG_DISABLE_NATIVE_DEPENDENCY_LINKER=1` - disable native
binlink
- `BUN_FEATURE_FLAG_DISABLE_IGNORE_SCRIPTS=1` - disable script ignoring
## Implementation Details
### Core Components
- **`postinstall_optimizer.zig`**: New file containing the optimizer
logic
- `PostinstallOptimizer` enum with `native_binlink` and `ignore`
variants
- `List` type to track optimization strategies per package hash
- Defaults for `esbuild` (native binlink) and `sharp` (ignore)
- **`Bin.Linker` changes**: Extended to support separate target paths
- `target_node_modules_path`: Where to find the actual binary
- `target_package_name`: Name of the package containing the binary
- Fallback logic when native binlink optimization fails
### Modified Components
- **PackageInstaller.zig**: Checks optimizer before:
- Enqueueing lifecycle scripts
- Linking binaries (with platform-specific package resolution)
- **isolated_install/Installer.zig**: Similar checks for isolated linker
mode
- `maybeReplaceNodeModulesPath()` resolves platform-specific packages
- Retry logic without optimization on failure
- **Lockfile**: Added `postinstall_optimizer` field to persist
configuration
## Changes Included
- Updated `esbuild` from 0.21.5 to 0.25.11 (testing with latest)
- VS Code launch config updates for debugging install with new flags
- New feature flags in `env_var.zig`
## Test Plan
- [x] Existing install tests pass
- [ ] Test esbuild install without postinstall scripts running
- [ ] Test sharp install with scripts skipped
- [ ] Test custom package.json configuration
- [ ] Test fallback when platform-specific package not found
- [ ] Test feature flag overrides
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Native binlink optimization: installs platform-specific binaries when
available, with a safe retry fallback and verbose logging option.
* Per-package postinstall controls to optionally skip lifecycle scripts.
* New feature flags to disable native binlink optimization and to
disable lifecycle-script ignoring.
* **Tests**
* End-to-end tests and test packages added to validate native binlink
behavior across install scenarios and linker modes.
* **Documentation**
* Bench README and sample app migrated to a Next.js-based setup.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
153 lines
4.8 KiB
TypeScript
153 lines
4.8 KiB
TypeScript
import { spawn } from "bun";
|
|
import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
|
|
import { rm, writeFile } from "fs/promises";
|
|
import { bunEnv, bunExe, isWindows, VerdaccioRegistry } from "harness";
|
|
import { join } from "path";
|
|
|
|
let verdaccio: VerdaccioRegistry;
|
|
|
|
beforeAll(async () => {
|
|
setDefaultTimeout(1000 * 60 * 5);
|
|
verdaccio = new VerdaccioRegistry();
|
|
await verdaccio.start();
|
|
});
|
|
|
|
afterAll(() => {
|
|
verdaccio.stop();
|
|
});
|
|
|
|
describe.skipIf(isWindows).concurrent("native binlink optimization", () => {
|
|
for (const linker of ["hoisted", "isolated"]) {
|
|
test(`uses platform-specific bin instead of main package bin with linker ${linker}`, async () => {
|
|
let env = { ...bunEnv };
|
|
const { packageDir, packageJson } = await verdaccio.createTestDir();
|
|
env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache");
|
|
env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp");
|
|
|
|
// Create bunfig
|
|
await writeFile(
|
|
join(packageDir, "bunfig.toml"),
|
|
`
|
|
[install]
|
|
cache = "${join(packageDir, ".bun-cache").replaceAll("\\", "\\\\")}"
|
|
registry = "${verdaccio.registryUrl()}"
|
|
linker = "${linker}"
|
|
`,
|
|
);
|
|
|
|
// Install the main package
|
|
await writeFile(
|
|
packageJson,
|
|
JSON.stringify({
|
|
name: "test-app",
|
|
version: "1.0.0",
|
|
dependencies: {
|
|
"test-native-binlink": "1.0.0",
|
|
},
|
|
nativeDependencies: ["test-native-binlink"],
|
|
trustedDependencies: ["test-native-binlink"],
|
|
}),
|
|
);
|
|
|
|
const installProc = spawn({
|
|
cmd: [bunExe(), "install"],
|
|
cwd: packageDir,
|
|
stdout: "inherit",
|
|
stdin: "ignore",
|
|
stderr: "inherit",
|
|
env,
|
|
});
|
|
|
|
expect(await installProc.exited).toBe(0);
|
|
|
|
// Run the bin - it should use the platform-specific one (exit code 0)
|
|
// not the main package one (exit code 1)
|
|
const binProc = spawn({
|
|
cmd: [join(packageDir, "node_modules", ".bin", "test-binlink-cmd")],
|
|
cwd: packageDir,
|
|
stdout: "pipe",
|
|
stdin: "ignore",
|
|
stderr: "inherit",
|
|
env,
|
|
});
|
|
|
|
const [binStdout, binExitCode] = await Promise.all([binProc.stdout.text(), binProc.exited]);
|
|
|
|
// Should exit with 0 (platform-specific) not 1 (main package)
|
|
expect(binExitCode).toBe(0);
|
|
expect(binStdout).toContain("SUCCESS: Using platform-specific bin");
|
|
|
|
// Now delete the node_modules folder, keep the bun.lock, re-install
|
|
await rm(join(packageDir, "node_modules"), { recursive: true, force: true });
|
|
const installProc2 = spawn({
|
|
cmd: [bunExe(), "install"],
|
|
cwd: packageDir,
|
|
stdout: "inherit",
|
|
stdin: "ignore",
|
|
stderr: "inherit",
|
|
env,
|
|
});
|
|
expect(await installProc2.exited).toBe(0);
|
|
|
|
const binProc2 = spawn({
|
|
cmd: [join(packageDir, "node_modules", ".bin", "test-binlink-cmd")],
|
|
cwd: packageDir,
|
|
stdout: "pipe",
|
|
stdin: "ignore",
|
|
stderr: "inherit",
|
|
env,
|
|
});
|
|
const [binStdout2, binExitCode2] = await Promise.all([binProc2.stdout.text(), binProc2.exited]);
|
|
expect(binStdout2).toContain("SUCCESS: Using platform-specific bin");
|
|
expect(binExitCode2).toBe(0);
|
|
|
|
// Now do a no-op re-install.
|
|
const installProc3 = spawn({
|
|
cmd: [bunExe(), "install"],
|
|
cwd: packageDir,
|
|
stdout: "inherit",
|
|
stdin: "ignore",
|
|
stderr: "inherit",
|
|
env,
|
|
});
|
|
expect(await installProc3.exited).toBe(0);
|
|
|
|
const binProc3 = spawn({
|
|
cmd: [join(packageDir, "node_modules", ".bin", "test-binlink-cmd")],
|
|
cwd: packageDir,
|
|
stdout: "pipe",
|
|
stdin: "ignore",
|
|
stderr: "inherit",
|
|
env,
|
|
});
|
|
const [binStdout3, binExitCode3] = await Promise.all([binProc3.stdout.text(), binProc3.exited]);
|
|
expect(binStdout3).toContain("SUCCESS: Using platform-specific bin");
|
|
expect(binExitCode3).toBe(0);
|
|
|
|
// Now do an install with the .bin folder gone
|
|
await rm(join(packageDir, "node_modules", ".bin"), { recursive: true, force: true });
|
|
const installProc4 = spawn({
|
|
cmd: [bunExe(), "install"],
|
|
cwd: packageDir,
|
|
stdout: "inherit",
|
|
stdin: "ignore",
|
|
stderr: "inherit",
|
|
env,
|
|
});
|
|
expect(await installProc4.exited).toBe(0);
|
|
|
|
const binProc4 = spawn({
|
|
cmd: [join(packageDir, "node_modules", ".bin", "test-binlink-cmd")],
|
|
cwd: packageDir,
|
|
stdout: "pipe",
|
|
stdin: "ignore",
|
|
stderr: "inherit",
|
|
env,
|
|
});
|
|
const [binStdout4, binExitCode4] = await Promise.all([binProc4.stdout.text(), binProc4.exited]);
|
|
expect(binStdout4).toContain("SUCCESS: Using platform-specific bin");
|
|
expect(binExitCode4).toBe(0);
|
|
});
|
|
}
|
|
});
|