Files
bun.sh/src/install/postinstall_optimizer.zig
Jarred Sumner 528620e9ae Add postinstall optimizer with native binlink support and script skipping (#24283)
## 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>
2025-11-03 20:36:22 -08:00

171 lines
5.8 KiB
Zig

pub const PostinstallOptimizer = enum {
native_binlink,
ignore,
const default_native_binlinks_name_hashes = &[_]PackageNameHash{
bun.Semver.String.Builder.stringHash("esbuild"),
};
const default_ignore_name_hashes = &[_]PackageNameHash{
bun.Semver.String.Builder.stringHash("sharp"),
};
fn fromStringArrayGroup(list: *List, expr: *const ast.Expr, allocator: std.mem.Allocator, value: PostinstallOptimizer) !bool {
var array = expr.asArray() orelse return false;
if (array.array.items.len == 0) {
return true;
}
while (array.next()) |entry| {
if (entry.isString()) {
const str = entry.asString(allocator) orelse continue;
if (str.len == 0) continue;
const hash = bun.Semver.String.Builder.stringHash(str);
try list.dynamic.put(allocator, hash, value);
}
}
return true;
}
pub fn fromPackageJSON(list: *List, expr: *const ast.Expr, allocator: std.mem.Allocator) !void {
if (expr.get("nativeDependencies")) |*native_deps_expr| {
list.disable_default_native_binlinks = try fromStringArrayGroup(list, native_deps_expr, allocator, .native_binlink);
}
if (expr.get("ignoreScripts")) |*ignored_scripts_expr| {
list.disable_default_ignore = try fromStringArrayGroup(list, ignored_scripts_expr, allocator, .ignore);
}
}
pub fn getNativeBinlinkReplacementPackageID(
resolutions: []const PackageID,
metas: []const Meta,
target_cpu: Npm.Architecture,
target_os: Npm.OperatingSystem,
) ?PackageID {
// Windows needs file extensions.
if (target_os.isMatch(@enumFromInt(Npm.OperatingSystem.win32))) {
return null;
}
// Loop through the list of optional dependencies with platform-specific constraints
// Find a matching target-specific dependency.
for (resolutions) |resolution| {
if (resolution > metas.len) continue;
const meta: *const Meta = &metas[resolution];
if (meta.arch == .all or meta.os == .all) continue;
if (meta.arch.isMatch(target_cpu) and meta.os.isMatch(target_os)) {
return resolution;
}
}
return null;
}
pub const List = struct {
dynamic: Map = .{},
disable_default_native_binlinks: bool = false,
disable_default_ignore: bool = false,
pub const Map = std.ArrayHashMapUnmanaged(PackageNameHash, PostinstallOptimizer, install.ArrayIdentityContext.U64, false);
pub fn isNativeBinlinkEnabled(this: *const @This()) bool {
if (this.dynamic.count() == 0) {
if (this.disable_default_native_binlinks) {
return true;
}
}
if (bun.env_var.feature_flag.BUN_FEATURE_FLAG_DISABLE_NATIVE_DEPENDENCY_LINKER.get()) {
return false;
}
return true;
}
pub fn shouldIgnoreLifecycleScripts(
this: *const @This(),
name_hash: PackageNameHash,
resolutions: []const PackageID,
metas: []const Meta,
target_cpu: Npm.Architecture,
target_os: Npm.OperatingSystem,
tree_id: ?Lockfile.Tree.Id,
) bool {
if (bun.env_var.feature_flag.BUN_FEATURE_FLAG_DISABLE_IGNORE_SCRIPTS.get()) {
return false;
}
const mode = this.get(name_hash) orelse return false;
return switch (mode) {
.native_binlink =>
// TODO: support hoisted.
(tree_id == null or tree_id.? == 0) and
// It's not as simple as checking `get(name_hash) != null` because if the
// specific versions of the package do not have optional
// dependencies then we cannot do this optimization without
// breaking the code.
//
// This shows up in test/integration/esbuild/esbuild.test.ts
getNativeBinlinkReplacementPackageID(resolutions, metas, target_cpu, target_os) != null,
.ignore => true,
};
}
fn fromDefault(name_hash: PackageNameHash) ?PostinstallOptimizer {
for (default_native_binlinks_name_hashes) |hash| {
if (hash == name_hash) {
return .native_binlink;
}
}
for (default_ignore_name_hashes) |hash| {
if (hash == name_hash) {
return .ignore;
}
}
return null;
}
pub fn get(this: *const @This(), name_hash: PackageNameHash) ?PostinstallOptimizer {
if (this.dynamic.get(name_hash)) |optimize| {
return optimize;
}
const default = fromDefault(name_hash) orelse {
return null;
};
switch (default) {
.native_binlink => {
if (!this.disable_default_native_binlinks) {
return .native_binlink;
}
},
.ignore => {
if (!this.disable_default_ignore) {
return .ignore;
}
},
}
return null;
}
};
};
const std = @import("std");
const bun = @import("bun");
const ast = bun.ast;
const install = bun.install;
const ArrayIdentityContext = install.ArrayIdentityContext;
const Lockfile = install.Lockfile;
const Npm = install.Npm;
const PackageID = install.PackageID;
const PackageNameHash = install.PackageNameHash;
const Meta = Lockfile.Package.Meta;