Plugins + cross-compilation + Bun.build API support for Bun.build({compile}) (#21915)

### What does this PR do?

in the name

### How did you verify your code works?

tests, but using ci to see if anything else broke

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
Michael H
2025-08-20 18:25:49 +10:00
committed by GitHub
parent e7672b2d04
commit d354714791
14 changed files with 855 additions and 214 deletions

View File

@@ -1628,12 +1628,24 @@ declare module "bun" {
kind: ImportKind;
}
namespace _BunBuildInterface {
type Architecture = "x64" | "arm64";
type Libc = "glibc" | "musl";
type SIMD = "baseline" | "modern";
type Target =
| `bun-darwin-${Architecture}`
| `bun-darwin-x64-${SIMD}`
| `bun-linux-${Architecture}`
| `bun-linux-${Architecture}-${Libc}`
| "bun-windows-x64"
| `bun-windows-x64-${SIMD}`
| `bun-linux-x64-${SIMD}-${Libc}`;
}
/**
* @see [Bun.build API docs](https://bun.com/docs/bundler#api)
*/
interface BuildConfig {
interface BuildConfigBase {
entrypoints: string[]; // list of file path
outdir?: string; // output directory
/**
* @default "browser"
*/
@@ -1671,7 +1683,6 @@ declare module "bun" {
asset?: string;
}; // | string;
root?: string; // project root
splitting?: boolean; // default true, enable code splitting
plugins?: BunPlugin[];
// manifest?: boolean; // whether to return manifest
external?: string[];
@@ -1820,8 +1831,57 @@ declare module "bun" {
* ```
*/
tsconfig?: string;
outdir?: string;
}
interface CompileBuildOptions {
target?: _BunBuildInterface.Target;
execArgv?: string[];
executablePath?: string;
outfile?: string;
windows?: {
hideConsole?: boolean;
icon?: string;
title?: string;
};
}
// Compile build config - uses outfile for executable output
interface CompileBuildConfig extends BuildConfigBase {
/**
* Create a standalone executable
*
* When `true`, creates an executable for the current platform.
* When a target string, creates an executable for that platform.
*
* @example
* ```ts
* // Create executable for current platform
* await Bun.build({
* entrypoints: ['./app.js'],
* compile: {
* target: 'linux-x64',
* },
* outfile: './my-app'
* });
*
* // Cross-compile for Linux x64
* await Bun.build({
* entrypoints: ['./app.js'],
* compile: 'linux-x64',
* outfile: './my-app'
* });
* ```
*/
compile: boolean | _BunBuildInterface.Target | CompileBuildOptions;
}
/**
* @see [Bun.build API docs](https://bun.com/docs/bundler#api)
*/
type BuildConfig = BuildConfigBase | CompileBuildConfig;
/**
* Hash and verify passwords using argon2 or bcrypt
*

View File

@@ -55,7 +55,7 @@ pub const StandaloneModuleGraph = struct {
// by normalized file path
pub fn find(this: *const StandaloneModuleGraph, name: []const u8) ?*File {
if (!isBunStandaloneFilePath(base_path)) {
if (!isBunStandaloneFilePath(name)) {
return null;
}
@@ -348,7 +348,7 @@ pub const StandaloneModuleGraph = struct {
var entry_point_id: ?usize = null;
var string_builder = bun.StringBuilder{};
var module_count: usize = 0;
for (output_files) |output_file| {
for (output_files) |*output_file| {
string_builder.countZ(output_file.dest_path);
string_builder.countZ(prefix);
if (output_file.value == .buffer) {
@@ -395,7 +395,7 @@ pub const StandaloneModuleGraph = struct {
var source_map_arena = bun.ArenaAllocator.init(allocator);
defer source_map_arena.deinit();
for (output_files) |output_file| {
for (output_files) |*output_file| {
if (!output_file.output_kind.isFileInStandaloneMode()) {
continue;
}
@@ -496,6 +496,21 @@ pub const StandaloneModuleGraph = struct {
windows_hide_console: bool = false,
};
pub const CompileResult = union(enum) {
success: void,
error_message: []const u8,
pub fn fail(msg: []const u8) CompileResult {
return .{ .error_message = msg };
}
pub fn deinit(this: *const @This()) void {
if (this.* == .error_message) {
bun.default_allocator.free(this.error_message);
}
}
};
pub fn inject(bytes: []const u8, self_exe: [:0]const u8, inject_options: InjectOptions, target: *const CompileTarget) bun.FileDescriptor {
var buf: bun.PathBuffer = undefined;
var zname: [:0]const u8 = bun.span(bun.fs.FileSystem.instance.tmpname("bun-build", &buf, @as(u64, @bitCast(std.time.milliTimestamp()))) catch |err| {
@@ -632,6 +647,7 @@ pub const StandaloneModuleGraph = struct {
cleanup(zname, fd);
Global.exit(1);
};
break :brk fd;
};
@@ -821,7 +837,43 @@ pub const StandaloneModuleGraph = struct {
var needs_download: bool = true;
const dest_z = target.exePath(&exe_path_buf, version_str, env, &needs_download);
if (needs_download) {
try target.downloadToPath(env, allocator, dest_z);
target.downloadToPath(env, allocator, dest_z) catch |err| {
// For CLI, provide detailed error messages and exit
switch (err) {
error.TargetNotFound => {
Output.errGeneric(
\\Does this target and version of Bun exist?
\\
\\404 downloading {} from npm registry
, .{target.*});
},
error.NetworkError => {
Output.errGeneric(
\\Failed to download cross-compilation target.
\\
\\Network error downloading {} from npm registry
, .{target.*});
},
error.InvalidResponse => {
Output.errGeneric(
\\Failed to verify the integrity of the downloaded tarball.
\\
\\The downloaded content for {} appears to be corrupted
, .{target.*});
},
error.ExtractionFailed => {
Output.errGeneric(
\\Failed to extract the downloaded tarball.
\\
\\Could not extract executable for {}
, .{target.*});
},
else => {
Output.errGeneric("Failed to download {}: {s}", .{ target.*, @errorName(err) });
},
}
Global.exit(1);
};
}
return try allocator.dupeZ(u8, dest_z);
@@ -839,27 +891,67 @@ pub const StandaloneModuleGraph = struct {
windows_hide_console: bool,
windows_icon: ?[]const u8,
compile_exec_argv: []const u8,
) !void {
const bytes = try toBytes(allocator, module_prefix, output_files, output_format, compile_exec_argv);
if (bytes.len == 0) return;
self_exe_path: ?[]const u8,
) !CompileResult {
const bytes = toBytes(allocator, module_prefix, output_files, output_format, compile_exec_argv) catch |err| {
return CompileResult.fail(std.fmt.allocPrint(allocator, "failed to generate module graph bytes: {s}", .{@errorName(err)}) catch "failed to generate module graph bytes");
};
if (bytes.len == 0) return CompileResult.fail("no output files to bundle");
defer allocator.free(bytes);
const fd = inject(
bytes,
if (target.isDefault())
var free_self_exe = false;
const self_exe = if (self_exe_path) |path| brk: {
free_self_exe = true;
break :brk allocator.dupeZ(u8, path) catch bun.outOfMemory();
} else if (target.isDefault())
bun.selfExePath() catch |err| {
Output.err(err, "failed to get self executable path", .{});
Global.exit(1);
return CompileResult.fail(std.fmt.allocPrint(allocator, "failed to get self executable path: {s}", .{@errorName(err)}) catch "failed to get self executable path");
}
else
download(allocator, target, env) catch |err| {
Output.err(err, "failed to download cross-compiled bun executable", .{});
Global.exit(1);
},
else blk: {
var exe_path_buf: bun.PathBuffer = undefined;
var version_str_buf: [1024]u8 = undefined;
const version_str = std.fmt.bufPrintZ(&version_str_buf, "{}", .{target}) catch {
return CompileResult.fail("failed to format target version string");
};
var needs_download: bool = true;
const dest_z = target.exePath(&exe_path_buf, version_str, env, &needs_download);
if (needs_download) {
target.downloadToPath(env, allocator, dest_z) catch |err| {
const msg = switch (err) {
error.TargetNotFound => std.fmt.allocPrint(allocator, "Target platform '{}' is not available for download. Check if this version of Bun supports this target.", .{target}) catch "Target platform not available for download",
error.NetworkError => std.fmt.allocPrint(allocator, "Network error downloading executable for '{}'. Check your internet connection and proxy settings.", .{target}) catch "Network error downloading executable",
error.InvalidResponse => std.fmt.allocPrint(allocator, "Downloaded file for '{}' appears to be corrupted. Please try again.", .{target}) catch "Downloaded file is corrupted",
error.ExtractionFailed => std.fmt.allocPrint(allocator, "Failed to extract executable for '{}'. The download may be incomplete.", .{target}) catch "Failed to extract downloaded executable",
error.UnsupportedTarget => std.fmt.allocPrint(allocator, "Target '{}' is not supported", .{target}) catch "Unsupported target",
else => std.fmt.allocPrint(allocator, "Failed to download '{}': {s}", .{ target, @errorName(err) }) catch "Download failed",
};
return CompileResult.fail(msg);
};
}
free_self_exe = true;
break :blk allocator.dupeZ(u8, dest_z) catch bun.outOfMemory();
};
defer if (free_self_exe) {
allocator.free(self_exe);
};
var fd = inject(
bytes,
self_exe,
.{ .windows_hide_console = windows_hide_console },
target,
);
defer if (fd != bun.invalid_fd) fd.close();
bun.debugAssert(fd.kind == .system);
if (Environment.isPosix) {
// Set executable permissions (0o755 = rwxr-xr-x) - makes it executable for owner, readable/executable for group and others
_ = Syscall.fchmod(fd, 0o755);
}
if (Environment.isWindows) {
var outfile_buf: bun.OSPathBuffer = undefined;
const outfile_slice = brk: {
@@ -871,52 +963,59 @@ pub const StandaloneModuleGraph = struct {
};
bun.windows.moveOpenedFileAtLoose(fd, .fromStdDir(root_dir), outfile_slice, true).unwrap() catch |err| {
if (err == error.EISDIR) {
Output.errGeneric("{} is a directory. Please choose a different --outfile or delete the directory", .{bun.fmt.utf16(outfile_slice)});
} else {
Output.err(err, "failed to move executable to result path", .{});
}
_ = bun.windows.deleteOpenedFile(fd);
Global.exit(1);
if (err == error.EISDIR) {
return CompileResult.fail(std.fmt.allocPrint(allocator, "{s} is a directory. Please choose a different --outfile or delete the directory", .{outfile}) catch "outfile is a directory");
} else {
return CompileResult.fail(std.fmt.allocPrint(allocator, "failed to move executable to result path: {s}", .{@errorName(err)}) catch "failed to move executable");
}
};
fd.close();
fd = bun.invalid_fd;
if (windows_icon) |icon_utf8| {
var icon_buf: bun.OSPathBuffer = undefined;
const icon = bun.strings.toWPathNormalized(&icon_buf, icon_utf8);
bun.windows.rescle.setIcon(outfile_slice, icon) catch {
Output.warn("Failed to set executable icon", .{});
bun.windows.rescle.setIcon(outfile_slice, icon) catch |err| {
Output.debug("Warning: Failed to set Windows icon for executable: {s}", .{@errorName(err)});
};
}
return;
return .success;
}
var buf: bun.PathBuffer = undefined;
const temp_location = bun.getFdPath(fd, &buf) catch |err| {
Output.prettyErrorln("<r><red>error<r><d>:<r> failed to get path for fd: {s}", .{@errorName(err)});
Global.exit(1);
return CompileResult.fail(std.fmt.allocPrint(allocator, "failed to get path for fd: {s}", .{@errorName(err)}) catch "failed to get path for file descriptor");
};
const temp_posix = std.posix.toPosixPath(temp_location) catch |err| {
return CompileResult.fail(std.fmt.allocPrint(allocator, "path too long: {s}", .{@errorName(err)}) catch "path too long");
};
const outfile_basename = std.fs.path.basename(outfile);
const outfile_posix = std.posix.toPosixPath(outfile_basename) catch |err| {
return CompileResult.fail(std.fmt.allocPrint(allocator, "outfile name too long: {s}", .{@errorName(err)}) catch "outfile name too long");
};
bun.sys.moveFileZWithHandle(
fd,
bun.FD.cwd(),
bun.sliceTo(&(try std.posix.toPosixPath(temp_location)), 0),
bun.sliceTo(&temp_posix, 0),
.fromStdDir(root_dir),
bun.sliceTo(&(try std.posix.toPosixPath(std.fs.path.basename(outfile))), 0),
bun.sliceTo(&outfile_posix, 0),
) catch |err| {
if (err == error.IsDir or err == error.EISDIR) {
Output.prettyErrorln("<r><red>error<r><d>:<r> {} is a directory. Please choose a different --outfile or delete the directory", .{bun.fmt.quote(outfile)});
} else {
Output.prettyErrorln("<r><red>error<r><d>:<r> failed to rename {s} to {s}: {s}", .{ temp_location, outfile, @errorName(err) });
}
_ = Syscall.unlink(
&(try std.posix.toPosixPath(temp_location)),
);
fd.close();
fd = bun.invalid_fd;
Global.exit(1);
_ = Syscall.unlink(&temp_posix);
if (err == error.IsDir or err == error.EISDIR) {
return CompileResult.fail(std.fmt.allocPrint(allocator, "{s} is a directory. Please choose a different --outfile or delete the directory", .{outfile}) catch "outfile is a directory");
} else {
return CompileResult.fail(std.fmt.allocPrint(allocator, "failed to rename {s} to {s}: {s}", .{ temp_location, outfile, @errorName(err) }) catch "failed to rename file");
}
};
return .success;
}
pub fn fromExecutable(allocator: std.mem.Allocator) !?StandaloneModuleGraph {

View File

@@ -37,6 +37,119 @@ pub const JSBundler = struct {
env_behavior: api.DotEnvBehavior = .disable,
env_prefix: OwnedString = OwnedString.initEmpty(bun.default_allocator),
tsconfig_override: OwnedString = OwnedString.initEmpty(bun.default_allocator),
compile: ?CompileOptions = null,
pub const CompileOptions = struct {
compile_target: CompileTarget = .{},
exec_argv: OwnedString = OwnedString.initEmpty(bun.default_allocator),
executable_path: OwnedString = OwnedString.initEmpty(bun.default_allocator),
windows_hide_console: bool = false,
windows_icon_path: OwnedString = OwnedString.initEmpty(bun.default_allocator),
windows_title: OwnedString = OwnedString.initEmpty(bun.default_allocator),
outfile: OwnedString = OwnedString.initEmpty(bun.default_allocator),
pub fn fromJS(globalThis: *jsc.JSGlobalObject, config: jsc.JSValue, allocator: std.mem.Allocator, compile_target: ?CompileTarget) JSError!?CompileOptions {
var this = CompileOptions{
.exec_argv = OwnedString.initEmpty(allocator),
.executable_path = OwnedString.initEmpty(allocator),
.windows_icon_path = OwnedString.initEmpty(allocator),
.windows_title = OwnedString.initEmpty(allocator),
.outfile = OwnedString.initEmpty(allocator),
.compile_target = compile_target orelse .{},
};
errdefer this.deinit();
const object = brk: {
const compile_value = try config.getTruthy(globalThis, "compile") orelse return null;
if (compile_value.isBoolean()) {
if (compile_value == .false) {
return null;
}
return this;
} else if (compile_value.isString()) {
this.compile_target = try CompileTarget.fromJS(globalThis, compile_value);
return this;
} else if (compile_value.isObject()) {
break :brk compile_value;
} else {
return globalThis.throwInvalidArguments("Expected compile to be a boolean or string or options object", .{});
}
};
if (try object.getOwn(globalThis, "target")) |target| {
this.compile_target = try CompileTarget.fromJS(globalThis, target);
}
if (try object.getOwnArray(globalThis, "execArgv")) |exec_argv| {
var iter = try exec_argv.arrayIterator(globalThis);
var is_first = true;
while (try iter.next()) |arg| {
var slice = try arg.toSlice(globalThis, bun.default_allocator);
defer slice.deinit();
if (is_first) {
is_first = false;
try this.exec_argv.appendSlice(slice.slice());
} else {
try this.exec_argv.appendChar(' ');
try this.exec_argv.appendSlice(slice.slice());
}
}
}
if (try object.getOwn(globalThis, "executablePath")) |executable_path| {
var slice = try executable_path.toSlice(globalThis, bun.default_allocator);
defer slice.deinit();
if (bun.sys.existsAtType(bun.FD.cwd(), slice.slice()).unwrapOr(.directory) != .file) {
return globalThis.throwInvalidArguments("executablePath must be a valid path to a Bun executable", .{});
}
try this.executable_path.appendSliceExact(slice.slice());
}
if (try object.getOwnTruthy(globalThis, "windows")) |windows| {
if (!windows.isObject()) {
return globalThis.throwInvalidArguments("windows must be an object", .{});
}
if (try windows.getOwn(globalThis, "hideConsole")) |hide_console| {
this.windows_hide_console = hide_console.toBoolean();
}
if (try windows.getOwn(globalThis, "icon")) |windows_icon_path| {
var slice = try windows_icon_path.toSlice(globalThis, bun.default_allocator);
defer slice.deinit();
if (bun.sys.existsAtType(bun.FD.cwd(), slice.slice()).unwrapOr(.directory) != .file) {
return globalThis.throwInvalidArguments("windows.icon must be a valid path to an ico file", .{});
}
try this.windows_icon_path.appendSliceExact(slice.slice());
}
if (try windows.getOwn(globalThis, "title")) |windows_title| {
var slice = try windows_title.toSlice(globalThis, bun.default_allocator);
defer slice.deinit();
try this.windows_title.appendSliceExact(slice.slice());
}
}
if (try object.getOwn(globalThis, "outfile")) |outfile| {
var slice = try outfile.toSlice(globalThis, bun.default_allocator);
defer slice.deinit();
try this.outfile.appendSliceExact(slice.slice());
}
return this;
}
pub fn deinit(this: *CompileOptions) void {
this.exec_argv.deinit();
this.executable_path.deinit();
this.windows_icon_path.deinit();
this.windows_title.deinit();
this.outfile.deinit();
}
};
pub const List = bun.StringArrayHashMapUnmanaged(Config);
@@ -58,9 +171,20 @@ pub const JSBundler = struct {
errdefer if (plugins.*) |plugin| plugin.deinit();
var did_set_target = false;
if (try config.getOptionalEnum(globalThis, "target", options.Target)) |target| {
this.target = target;
if (try config.getOptional(globalThis, "target", ZigString.Slice)) |slice| {
defer slice.deinit();
if (strings.hasPrefixComptime(slice.slice(), "bun-")) {
this.compile = .{
.compile_target = try CompileTarget.fromSlice(globalThis, slice.slice()),
};
this.target = .bun;
did_set_target = true;
} else {
this.target = options.Target.Map.get(slice.slice()) orelse {
return globalThis.throwInvalidArguments("Expected target to be one of 'browser', 'node', 'bun', 'macro', or 'bun-<target>', got {s}", .{slice.slice()});
};
did_set_target = true;
}
}
// Plugins must be resolved first as they are allowed to mutate the config JSValue
@@ -450,6 +574,52 @@ pub const JSBundler = struct {
this.throw_on_error = flag;
}
if (try CompileOptions.fromJS(
globalThis,
config,
bun.default_allocator,
if (this.compile) |*compile| compile.compile_target else null,
)) |compile| {
this.compile = compile;
}
if (this.compile) |*compile| {
this.target = .bun;
const define_keys = compile.compile_target.defineKeys();
const define_values = compile.compile_target.defineValues();
for (define_keys, define_values) |key, value| {
try this.define.insert(key, value);
}
const base_public_path = bun.StandaloneModuleGraph.targetBasePublicPath(this.compile.?.compile_target.os, "root/");
try this.public_path.append(base_public_path);
if (compile.outfile.isEmpty()) {
const entry_point = this.entry_points.keys()[0];
var outfile = std.fs.path.basename(entry_point);
const ext = std.fs.path.extension(outfile);
if (ext.len > 0) {
outfile = outfile[0 .. outfile.len - ext.len];
}
if (strings.eqlComptime(outfile, "index")) {
outfile = std.fs.path.basename(std.fs.path.dirname(entry_point) orelse "index");
}
if (strings.eqlComptime(outfile, "bun")) {
outfile = std.fs.path.basename(std.fs.path.dirname(entry_point) orelse "bun");
}
// If argv[0] is "bun" or "bunx", we don't check if the binary is standalone
if (strings.eqlComptime(outfile, "bun") or strings.eqlComptime(outfile, "bunx")) {
return globalThis.throwInvalidArguments("cannot use compile with an output file named 'bun' because bun won't realize it's a standalone executable. Please choose a different name for compile.outfile", .{});
}
try compile.outfile.appendSliceExact(outfile);
}
}
return this;
}
@@ -506,6 +676,9 @@ pub const JSBundler = struct {
self.conditions.deinit();
self.drop.deinit();
self.banner.deinit();
if (self.compile) |*compile| {
compile.deinit();
}
self.env_prefix.deinit();
self.footer.deinit();
self.tsconfig_override.deinit();
@@ -1281,6 +1454,7 @@ pub const BuildArtifact = struct {
const string = []const u8;
const CompileTarget = @import("../../compile_target.zig");
const Fs = @import("../../fs.zig");
const resolve_path = @import("../../resolver/resolve_path.zig");
const std = @import("std");

View File

@@ -1738,6 +1738,7 @@ pub const BundleV2 = struct {
transpiler.options.public_path = config.public_path.list.items;
transpiler.options.output_format = config.format;
transpiler.options.bytecode = config.bytecode;
transpiler.options.compile = config.compile != null;
transpiler.options.output_dir = config.outdir.slice();
transpiler.options.root_dir = config.rootdir.slice();
@@ -1782,6 +1783,114 @@ pub const BundleV2 = struct {
bun.destroy(this);
}
fn doCompilation(this: *JSBundleCompletionTask, output_files: *std.ArrayList(options.OutputFile)) bun.StandaloneModuleGraph.CompileResult {
const compile_options = &(this.config.compile orelse @panic("Unexpected: No compile options provided"));
const entry_point_index: usize = brk: {
for (output_files.items, 0..) |*output_file, i| {
if (output_file.output_kind == .@"entry-point" and (output_file.side orelse .server) == .server) {
break :brk i;
}
}
return bun.StandaloneModuleGraph.CompileResult.fail("No entry point found for compilation");
};
const output_file = &output_files.items[entry_point_index];
const outbuf = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(outbuf);
var full_outfile_path = if (this.config.outdir.slice().len > 0)
bun.path.joinAbsStringBuf(this.config.outdir.slice(), outbuf, &[_][]const u8{compile_options.outfile.slice()}, .loose)
else
compile_options.outfile.slice();
// Add .exe extension for Windows targets if not already present
if (compile_options.compile_target.os == .windows and !strings.hasSuffixComptime(full_outfile_path, ".exe")) {
full_outfile_path = std.fmt.allocPrint(bun.default_allocator, "{s}.exe", .{full_outfile_path}) catch bun.outOfMemory();
} else {
full_outfile_path = bun.default_allocator.dupe(u8, full_outfile_path) catch bun.outOfMemory();
}
const dirname = std.fs.path.dirname(full_outfile_path) orelse ".";
const basename = std.fs.path.basename(full_outfile_path);
var root_dir = bun.FD.cwd().stdDir();
defer {
if (bun.FD.fromStdDir(root_dir) != bun.FD.cwd()) {
root_dir.close();
}
}
if (!(dirname.len == 0 or strings.eqlComptime(dirname, "."))) {
root_dir = root_dir.makeOpenPath(dirname, .{}) catch |err| {
return bun.StandaloneModuleGraph.CompileResult.fail(std.fmt.allocPrint(bun.default_allocator, "Failed to open output directory {s}: {s}", .{ dirname, @errorName(err) }) catch bun.outOfMemory());
};
}
const result = bun.StandaloneModuleGraph.toExecutable(
&compile_options.compile_target,
bun.default_allocator,
output_files.items,
root_dir,
this.config.public_path.slice(),
basename,
this.env,
this.config.format,
compile_options.windows_hide_console,
if (compile_options.windows_icon_path.slice().len > 0)
compile_options.windows_icon_path.slice()
else
null,
compile_options.exec_argv.slice(),
if (compile_options.executable_path.slice().len > 0)
compile_options.executable_path.slice()
else
null,
) catch |err| {
return bun.StandaloneModuleGraph.CompileResult.fail(std.fmt.allocPrint(bun.default_allocator, "{s}", .{@errorName(err)}) catch bun.outOfMemory());
};
if (result == .success) {
output_file.dest_path = full_outfile_path;
output_file.is_executable = true;
}
for (output_files.items, 0..) |*current, i| {
if (i != entry_point_index) {
current.deinit();
}
}
const entry_point_output_file = output_files.swapRemove(entry_point_index);
output_files.items.len = 1;
output_files.items[0] = entry_point_output_file;
return result;
}
fn toJSError(this: *JSBundleCompletionTask, promise: *jsc.JSPromise, globalThis: *jsc.JSGlobalObject) void {
if (this.config.throw_on_error) {
promise.reject(globalThis, this.log.toJSAggregateError(globalThis, bun.String.static("Bundle failed")));
return;
}
const root_obj = jsc.JSValue.createEmptyObject(globalThis, 3);
root_obj.put(globalThis, jsc.ZigString.static("outputs"), jsc.JSValue.createEmptyArray(globalThis, 0) catch return promise.reject(globalThis, error.JSError));
root_obj.put(
globalThis,
jsc.ZigString.static("success"),
jsc.JSValue.jsBoolean(false),
);
root_obj.put(
globalThis,
jsc.ZigString.static("logs"),
this.log.toJSArray(globalThis, bun.default_allocator) catch |err| {
return promise.reject(globalThis, err);
},
);
promise.resolve(globalThis, root_obj);
}
pub fn onComplete(this: *JSBundleCompletionTask) void {
var globalThis = this.globalThis;
defer this.deref();
@@ -1799,33 +1908,25 @@ pub const BundleV2 = struct {
const promise = this.promise.swap();
switch (this.result) {
.pending => unreachable,
.err => brk: {
if (this.config.throw_on_error) {
promise.reject(globalThis, this.log.toJSAggregateError(globalThis, bun.String.static("Bundle failed")));
break :brk;
if (this.result == .value) {
if (this.config.compile != null) {
var compile_result = this.doCompilation(&this.result.value.output_files);
defer compile_result.deinit();
if (compile_result != .success) {
this.log.addError(null, Logger.Loc.Empty, this.log.msgs.allocator.dupe(u8, compile_result.error_message) catch bun.outOfMemory()) catch bun.outOfMemory();
this.result.value.deinit();
this.result = .{ .err = error.CompilationFailed };
}
}
}
const root_obj = jsc.JSValue.createEmptyObject(globalThis, 3);
root_obj.put(globalThis, jsc.ZigString.static("outputs"), jsc.JSValue.createEmptyArray(globalThis, 0) catch return promise.reject(globalThis, error.JSError));
root_obj.put(
globalThis,
jsc.ZigString.static("success"),
jsc.JSValue.jsBoolean(false),
);
root_obj.put(
globalThis,
jsc.ZigString.static("logs"),
this.log.toJSArray(globalThis, bun.default_allocator) catch |err| {
return promise.reject(globalThis, err);
},
);
promise.resolve(globalThis, root_obj);
},
switch (this.result) {
.pending => unreachable,
.err => this.toJSError(promise, globalThis),
.value => |*build| {
const root_obj = jsc.JSValue.createEmptyObject(globalThis, 3);
const output_files: []options.OutputFile = build.output_files.items;
const output_files = build.output_files.items;
const output_files_js = jsc.JSValue.createEmptyArray(globalThis, output_files.len) catch return promise.reject(globalThis, error.JSError);
if (output_files_js == .zero) {
@panic("Unexpected pending JavaScript exception in JSBundleCompletionTask.onComplete. This is a bug in Bun.");
@@ -1848,7 +1949,7 @@ pub const BundleV2 = struct {
bun.default_allocator.dupe(
u8,
bun.path.joinAbsString(
Fs.FileSystem.instance.top_level_dir,
bun.fs.FileSystem.instance.top_level_dir,
&[_]string{ this.config.dir.slice(), this.config.outdir.slice(), output_file.dest_path },
.auto,
),

View File

@@ -318,7 +318,9 @@ pub fn generateChunksInParallel(
var static_route_visitor = StaticRouteVisitor{ .c = c, .visited = bun.bit_set.AutoBitSet.initEmpty(bun.default_allocator, c.graph.files.len) catch bun.outOfMemory() };
defer static_route_visitor.deinit();
if (root_path.len > 0) {
// Don't write to disk if compile mode is enabled - we need buffer values for compilation
const is_compile = bundler.transpiler.options.compile;
if (root_path.len > 0 and !is_compile) {
try c.writeOutputFilesToDisk(root_path, chunks, &output_files);
} else {
// In-memory build

View File

@@ -1,10 +1,4 @@
pub const BuildCommand = struct {
const compile_define_keys = &.{
"process.platform",
"process.arch",
"process.versions.bun",
};
pub fn exec(ctx: Command.Context, fetcher: ?*BundleV2.DependenciesScanner) !void {
Global.configureAllocator(.{ .long_running = true });
const allocator = ctx.allocator;
@@ -26,7 +20,9 @@ pub const BuildCommand = struct {
const compile_target = &ctx.bundler_options.compile_target;
if (ctx.bundler_options.compile) {
const compile_define_keys = compile_target.defineKeys();
const compile_define_values = compile_target.defineValues();
if (ctx.args.define) |*define| {
var keys = try std.ArrayList(string).initCapacity(bun.default_allocator, compile_define_keys.len + define.keys.len);
keys.appendSliceAssumeCapacity(compile_define_keys);
@@ -426,7 +422,7 @@ pub const BuildCommand = struct {
}
}
try bun.StandaloneModuleGraph.toExecutable(
const result = bun.StandaloneModuleGraph.toExecutable(
compile_target,
allocator,
output_files,
@@ -438,7 +434,17 @@ pub const BuildCommand = struct {
ctx.bundler_options.windows_hide_console,
ctx.bundler_options.windows_icon,
ctx.bundler_options.compile_exec_argv orelse "",
);
null,
) catch |err| {
Output.printErrorln("failed to create executable: {s}", .{@errorName(err)});
Global.exit(1);
};
if (result != .success) {
Output.printErrorln("{s}", .{result.error_message});
Global.exit(1);
}
const compiled_elapsed = @divTrunc(@as(i64, @truncate(std.time.nanoTimestamp() - bundled_end)), @as(i64, std.time.ns_per_ms));
const compiled_elapsed_digit_count: isize = switch (compiled_elapsed) {
0...9 => 3,

View File

@@ -1,15 +1,10 @@
const CompileTarget = @This();
/// Used for `bun build --compile`
///
/// This downloads and extracts the bun binary for the target platform
/// It uses npm to download the bun binary from the npm registry
/// It stores the downloaded binary into the bun install cache.
///
const bun = @import("bun");
const Environment = bun.Environment;
const strings = bun.strings;
const Output = bun.Output;
const CompileTarget = @This();
os: Environment.OperatingSystem = Environment.os,
arch: Environment.Architecture = Environment.arch,
@@ -52,6 +47,16 @@ const BaselineFormatter = struct {
}
};
pub const DownloadError = error{
TargetNotFound,
NetworkError,
InvalidResponse,
ExtractionFailed,
InvalidTarget,
OutOfMemory,
NoSpaceLeft,
};
pub fn eql(this: *const CompileTarget, other: *const CompileTarget) bool {
return this.os == other.os and this.arch == other.arch and this.baseline == other.baseline and this.version.eql(other.version) and this.libc == other.libc;
}
@@ -70,12 +75,17 @@ pub fn toNPMRegistryURL(this: *const CompileTarget, buf: []u8) ![]const u8 {
}
pub fn toNPMRegistryURLWithURL(this: *const CompileTarget, buf: []u8, registry_url: []const u8) ![]const u8 {
// Validate the target is supported before building URL
if (!this.isSupported()) {
return error.UnsupportedTarget;
}
return switch (this.os) {
inline else => |os| switch (this.arch) {
inline else => |arch| switch (this.libc) {
inline else => |libc| switch (this.baseline) {
// https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-0.1.6.tgz
inline else => |is_baseline| try std.fmt.bufPrint(buf, comptime "{s}/@oven/bun-" ++
inline else => |is_baseline| std.fmt.bufPrint(buf, comptime "{s}/@oven/bun-" ++
os.npmName() ++ "-" ++ arch.npmName() ++
libc.npmName() ++
(if (is_baseline) "-baseline" else "") ++
@@ -89,7 +99,13 @@ pub fn toNPMRegistryURLWithURL(this: *const CompileTarget, buf: []u8, registry_u
this.version.major,
this.version.minor,
this.version.patch,
}),
}) catch |err| {
// Catch buffer overflow or other formatting errors
if (err == error.NoSpaceLeft) {
return error.BufferTooSmall;
}
return err;
},
},
},
},
@@ -145,9 +161,6 @@ pub fn exePath(this: *const CompileTarget, buf: *bun.PathBuffer, version_str: [:
return dest;
}
const HTTP = bun.http;
const MutableString = bun.MutableString;
const Global = bun.Global;
pub fn downloadToPath(this: *const CompileTarget, env: *bun.DotEnv.Loader, allocator: std.mem.Allocator, dest_z: [:0]const u8) !void {
HTTP.HTTPThread.init(&.{});
var refresher = bun.Progress{};
@@ -160,8 +173,12 @@ pub fn downloadToPath(this: *const CompileTarget, env: *bun.DotEnv.Loader, alloc
var compressed_archive_bytes = try allocator.create(MutableString);
compressed_archive_bytes.* = try MutableString.init(allocator, 24 * 1024 * 1024);
var url_buffer: [2048]u8 = undefined;
const url_str = try bun.default_allocator.dupe(u8, try this.toNPMRegistryURL(&url_buffer));
const url = bun.URL.parse(url_str);
const url_str = this.toNPMRegistryURL(&url_buffer) catch |err| {
// Return error without printing - let caller decide how to handle
return err;
};
const url_str_copy = try bun.default_allocator.dupe(u8, url_str);
const url = bun.URL.parse(url_str_copy);
{
var progress = refresher.start("Downloading", 0);
defer progress.end();
@@ -186,30 +203,15 @@ pub fn downloadToPath(this: *const CompileTarget, env: *bun.DotEnv.Loader, alloc
switch (response.status_code) {
404 => {
Output.errGeneric(
\\Does this target and version of Bun exist?
\\
\\404 downloading {} from {s}
, .{
this.*,
url_str,
});
Global.exit(1);
// Return error without printing - let caller handle the messaging
return error.TargetNotFound;
},
403, 429, 499...599 => |status| {
Output.errGeneric(
\\Failed to download cross-compilation target.
\\
\\HTTP {d} downloading {} from {s}
, .{
status,
this.*,
url_str,
});
Global.exit(1);
403, 429, 499...599 => {
// Return error without printing - let caller handle the messaging
return error.NetworkError;
},
200 => {},
else => return error.HTTPError,
else => return error.NetworkError,
}
}
@@ -219,44 +221,22 @@ pub fn downloadToPath(this: *const CompileTarget, env: *bun.DotEnv.Loader, alloc
defer compressed_archive_bytes.list.deinit(allocator);
if (compressed_archive_bytes.list.items.len == 0) {
Output.errGeneric(
\\Failed to verify the integrity of the downloaded tarball.
\\
\\Received empty content downloading {} from {s}
, .{
this.*,
url_str,
});
Global.exit(1);
// Return error without printing - let caller handle the messaging
return error.InvalidResponse;
}
{
var node = refresher.start("Decompressing", 0);
defer node.end();
var gunzip = bun.zlib.ZlibReaderArrayList.init(compressed_archive_bytes.list.items, &tarball_bytes, allocator) catch |err| {
var gunzip = bun.zlib.ZlibReaderArrayList.init(compressed_archive_bytes.list.items, &tarball_bytes, allocator) catch {
node.end();
Output.err(err,
\\Failed to decompress the downloaded tarball
\\
\\After downloading {} from {s}
, .{
this.*,
url_str,
});
Global.exit(1);
// Return error without printing - let caller handle the messaging
return error.InvalidResponse;
};
gunzip.readAll() catch |err| {
gunzip.readAll() catch {
node.end();
// One word difference so if someone reports the bug we can tell if it happened in init or readAll.
Output.err(err,
\\Failed to deflate the downloaded tarball
\\
\\After downloading {} from {s}
, .{
this.*,
url_str,
});
Global.exit(1);
// Return error without printing - let caller handle the messaging
return error.InvalidResponse;
};
gunzip.deinit();
}
@@ -282,22 +262,15 @@ pub fn downloadToPath(this: *const CompileTarget, env: *bun.DotEnv.Loader, alloc
// "package/bin"
.depth_to_skip = 2,
},
) catch |err| {
) catch {
node.end();
Output.err(err,
\\Failed to extract the downloaded tarball
\\
\\After downloading {} from {s}
, .{
this.*,
url_str,
});
Global.exit(1);
// Return error without printing - let caller handle the messaging
return error.ExtractionFailed;
};
var did_retry = false;
while (true) {
bun.sys.moveFileZ(.fromStdDir(tmpdir), if (this.os == .windows) "bun.exe" else "bun", bun.invalid_fd, dest_z) catch |err| {
bun.sys.moveFileZ(.fromStdDir(tmpdir), if (this.os == .windows) "bun.exe" else "bun", bun.invalid_fd, dest_z) catch {
if (!did_retry) {
did_retry = true;
const dirname = bun.path.dirname(dest_z, .loose);
@@ -309,8 +282,8 @@ pub fn downloadToPath(this: *const CompileTarget, env: *bun.DotEnv.Loader, alloc
// fallthrough, failed for another reason
}
node.end();
Output.err(err, "Failed to move cross-compiled bun binary into cache directory {}", .{bun.fmt.fmtPath(u8, dest_z, .{})});
Global.exit(1);
// Return error without printing - let caller handle the messaging
return error.ExtractionFailed;
};
break;
}
@@ -331,9 +304,13 @@ pub fn isSupported(this: *const CompileTarget) bool {
};
}
pub fn from(input_: []const u8) CompileTarget {
var this = CompileTarget{};
pub const ParseError = error{
UnsupportedTarget,
InvalidTarget,
};
pub fn tryFrom(input_: []const u8) ParseError!CompileTarget {
var this = CompileTarget{};
const input = bun.strings.trim(input_, " \t\r");
if (input.len == 0) {
return this;
@@ -373,8 +350,7 @@ pub fn from(input_: []const u8) CompileTarget {
const version = bun.Semver.Version.parse(bun.Semver.SlicedString.init(token[1..], token[1..]));
if (version.valid) {
if (version.version.major == null or version.version.minor == null or version.version.patch == null) {
Output.errGeneric("Please pass a complete version number to --target. For example, --target=bun-v" ++ Environment.version_string, .{});
Global.exit(1);
return error.InvalidTarget;
}
this.version = .{
@@ -390,21 +366,15 @@ pub fn from(input_: []const u8) CompileTarget {
found_libc = true;
continue;
} else {
Output.errGeneric(
\\Unsupported target {} in "bun{s}"
\\To see the supported targets:
\\ https://bun.com/docs/bundler/executables
,
.{
bun.fmt.quote(token),
// received input starts at "-"
input_,
},
);
Global.exit(1);
return error.UnsupportedTarget;
}
}
if (!found_libc and this.libc == .musl and this.os != .linux) {
// "bun-windows-x64" should not implicitly be "bun-windows-x64-musl"
this.libc = .default;
}
if (found_os and !found_arch) {
// default to x64 if no arch is specified but OS is specified
// On macOS arm64, it's kind of surprising to choose Linux arm64 or Windows arm64
@@ -418,18 +388,77 @@ pub fn from(input_: []const u8) CompileTarget {
}
if (this.libc == .musl and this.os != .linux) {
Output.errGeneric("invalid target, musl libc only exists on linux", .{});
Global.exit(1);
return error.InvalidTarget;
}
if (this.arch == .wasm or this.os == .wasm) {
Output.errGeneric("invalid target, WebAssembly is not supported. Sorry!", .{});
Global.exit(1);
return error.InvalidTarget;
}
return this;
}
pub fn from(input_: []const u8) CompileTarget {
return tryFrom(input_) catch |err| {
switch (err) {
ParseError.UnsupportedTarget => {
const input = bun.strings.trim(input_, " \t\r");
var splitter = bun.strings.split(input, "-");
var unsupported_token: ?[]const u8 = null;
while (splitter.next()) |token| {
if (token.len == 0) continue;
if (Environment.Architecture.names.get(token) == null and
Environment.OperatingSystem.names.get(token) == null and
!strings.eqlComptime(token, "modern") and
!strings.eqlComptime(token, "baseline") and
!strings.eqlComptime(token, "musl") and
!(strings.hasPrefixComptime(token, "v1.") or strings.hasPrefixComptime(token, "v0.")))
{
unsupported_token = token;
break;
}
}
if (unsupported_token) |token| {
Output.errGeneric(
\\Unsupported target {} in "bun{s}"
\\To see the supported targets:
\\ https://bun.com/docs/bundler/executables
, .{
bun.fmt.quote(token),
input_,
});
} else {
Output.errGeneric("Unsupported target: {s}", .{input_});
}
Global.exit(1);
},
ParseError.InvalidTarget => {
const input = bun.strings.trim(input_, " \t\r");
if (strings.containsComptime(input, "musl") and !strings.containsComptime(input, "linux")) {
Output.errGeneric("invalid target, musl libc only exists on linux", .{});
} else if (strings.containsComptime(input, "wasm")) {
Output.errGeneric("invalid target, WebAssembly is not supported. Sorry!", .{});
} else if (strings.containsComptime(input, "v")) {
Output.errGeneric("Please pass a complete version number to --target. For example, --target=bun-v" ++ Environment.version_string, .{});
} else {
Output.errGeneric("Invalid target: {s}", .{input_});
}
Global.exit(1);
},
}
};
}
// Exists for consistentcy with values.
pub fn defineKeys(_: *const CompileTarget) []const []const u8 {
return &.{
"process.platform",
"process.arch",
"process.versions.bun",
};
}
pub fn defineValues(this: *const CompileTarget) []const []const u8 {
// Use inline else to avoid extra allocations.
switch (this.os) {
@@ -452,4 +481,35 @@ pub fn defineValues(this: *const CompileTarget) []const []const u8 {
}
}
pub fn fromJS(global: *jsc.JSGlobalObject, value: jsc.JSValue) bun.JSError!CompileTarget {
const slice = try value.toSlice(global, bun.default_allocator);
defer slice.deinit();
if (!strings.hasPrefixComptime(slice.slice(), "bun-")) {
return global.throwInvalidArguments("Expected compile target to start with 'bun-', got {s}", .{slice.slice()});
}
return fromSlice(global, slice.slice());
}
pub fn fromSlice(global: *jsc.JSGlobalObject, slice_with_bun_prefix: []const u8) bun.JSError!CompileTarget {
const slice = slice_with_bun_prefix["bun-".len..];
const target_parsed = tryFrom(slice) catch {
return global.throwInvalidArguments("Unknown compile target: {s}", .{slice_with_bun_prefix});
};
if (!target_parsed.isSupported()) {
return global.throwInvalidArguments("Unsupported compile target: {s}", .{slice_with_bun_prefix});
}
return target_parsed;
}
const std = @import("std");
const bun = @import("bun");
const Environment = bun.Environment;
const Global = bun.Global;
const HTTP = bun.http;
const MutableString = bun.MutableString;
const Output = bun.Output;
const jsc = bun.jsc;
const strings = bun.strings;

View File

@@ -0,0 +1,64 @@
import { describe, expect, test } from "bun:test";
import { isArm64, isLinux, isMacOS, isMusl, isWindows, tempDir } from "harness";
import { join } from "path";
describe("Bun.build compile", () => {
test("compile with current platform target string", async () => {
using dir = tempDir("build-compile-target", {
"app.js": `console.log("Cross-compiled app");`,
});
const os = isMacOS ? "darwin" : isLinux ? "linux" : isWindows ? "windows" : "unknown";
const arch = isArm64 ? "aarch64" : "x64";
const musl = isMusl ? "-musl" : "";
const target = `bun-${os}-${arch}${musl}` as any;
const outdir = join(dir + "", "out");
const result = await Bun.build({
entrypoints: [join(dir + "", "app.js")],
outdir,
compile: {
target: target,
outfile: "app-cross",
},
});
expect(result.success).toBe(true);
expect(result.outputs.length).toBe(1);
expect(result.outputs[0].path).toEndWith(isWindows ? "app-cross.exe" : "app-cross");
const exists = await Bun.file(result.outputs[0].path).exists();
// Verify that we do write it to the outdir.
expect(result.outputs[0].path.replaceAll("\\", "/")).toStartWith(outdir.replaceAll("\\", "/"));
expect(exists).toBe(true);
});
test("compile with invalid target fails gracefully", async () => {
using dir = tempDir("build-compile-invalid", {
"index.js": `console.log("test");`,
});
expect(() =>
Bun.build({
entrypoints: [join(dir, "index.js")],
compile: {
target: "bun-invalid-platform",
outfile: join(dir, "invalid-app"),
},
}),
).toThrowErrorMatchingInlineSnapshot(`"Unknown compile target: bun-invalid-platform"`);
expect(() =>
Bun.build({
entrypoints: [join(dir, "index.js")],
compile: {
target: "bun-windows-arm64",
outfile: join(dir, "invalid-app"),
},
}),
).toThrowErrorMatchingInlineSnapshot(`"Unsupported compile target: bun-windows-arm64"`);
});
});
// file command test works well

View File

@@ -17,7 +17,6 @@ describe("bundler", () => {
itBundled("compile/HelloWorldWithProcessVersionsBun", {
compile: true,
files: {
[`/${process.platform}-${process.arch}.js`]: "module.exports = process.versions.bun;",
"/entry.ts": /* js */ `
process.exitCode = 1;
process.versions.bun = "bun!";
@@ -26,9 +25,49 @@ describe("bundler", () => {
process.exitCode = 0;
}
`,
[`/${process.platform}-${process.arch}.js`]: "module.exports = process.versions.bun;",
},
run: { exitCode: 0 },
});
itBundled("compile/HelloWorldWithProcessVersionsBunAPI", {
compile: true,
backend: "api",
outfile: "dist/out",
files: {
"/entry.ts": /* js */ `
import { foo } from "hello:world";
if (foo !== "bar") throw new Error("fail");
process.exitCode = 1;
process.versions.bun = "bun!";
if (process.versions.bun === "bun!") throw new Error("fail");
const another = require("./${process.platform}-${process.arch}.js").replaceAll("-debug", "");
if (another === "${Bun.version.replaceAll("-debug", "")}") {
process.exitCode = 0;
}
`,
[`/${process.platform}-${process.arch}.js`]: "module.exports = process.versions.bun;",
},
run: { exitCode: 0, stdout: "hello world" },
plugins: [
{
name: "hello-world",
setup(api) {
api.onResolve({ filter: /hello:world/, namespace: "file" }, args => {
return {
path: args.path,
namespace: "hello",
};
});
api.onLoad({ filter: /.*/, namespace: "hello" }, args => {
return {
contents: "export const foo = 'bar'; console.log('hello world');",
loader: "js",
};
});
},
},
],
});
itBundled("compile/HelloWorldBytecode", {
compile: true,
bytecode: true,

View File

@@ -2,8 +2,10 @@ import { describe } from "bun:test";
import { itBundled } from "./expectBundled";
describe("bundler", () => {
itBundled("compile/HTMLServerBasic", {
for (const backend of ["api", "cli"] as const) {
itBundled(`compile/${backend}/HTMLServerBasic`, {
compile: true,
backend: backend,
files: {
"/entry.ts": /* js */ `
import index from "./index.html";
@@ -51,8 +53,9 @@ describe("bundler", () => {
},
});
itBundled("compile/HTMLServerMultipleRoutes", {
itBundled(`compile/${backend}/HTMLServerMultipleRoutes`, {
compile: true,
backend: backend,
files: {
"/entry.ts": /* js */ `
import home from "./home.html";
@@ -118,4 +121,5 @@ describe("bundler", () => {
stdout: "Home status: 200\nHome has content: true\nAbout status: 200\nAbout has content: true",
},
});
}
});

View File

@@ -4,8 +4,9 @@ import { itBundled } from "./expectBundled";
describe("bundler", () => {
// Test that the --compile-exec-argv flag works for both runtime processing and execArgv
itBundled("compile/CompileExecArgvDualBehavior", {
compile: true,
compileArgv: "--title=CompileExecArgvDualBehavior --smol",
compile: {
execArgv: ["--title=CompileExecArgvDualBehavior", "--smol"],
},
files: {
"/entry.ts": /* js */ `
// Test that --compile-exec-argv both processes flags AND populates execArgv

View File

@@ -1,7 +1,7 @@
/**
* See `./expectBundled.md` for how this works.
*/
import { BuildConfig, BuildOutput, BunPlugin, fileURLToPath, PluginBuilder, Loader } from "bun";
import { BuildConfig, BuildOutput, BunPlugin, fileURLToPath, PluginBuilder, Loader, CompileBuildOptions } from "bun";
import { callerSourceOrigin } from "bun:jsc";
import type { Matchers } from "bun:test";
import * as esbuild from "esbuild";
@@ -148,9 +148,8 @@ export interface BundlerTestInput {
/** Use when doing something weird with entryPoints and you need to check other output paths. */
outputPaths?: string[];
/** Use --compile */
compile?: boolean;
/** Use --compile-exec-argv to prepend arguments to standalone executable */
compileArgv?: string | string[];
compile?: boolean | string | CompileBuildOptions;
/** force using cli or js api. defaults to api if possible, then cli otherwise */
backend?: "cli" | "api";
@@ -432,7 +431,6 @@ function expectBundled(
chunkNaming,
cjs2esm,
compile,
compileArgv,
conditions,
dce,
dceKeepMarkerCount,
@@ -696,8 +694,8 @@ function expectBundled(
...(entryPointsRaw ?? []),
bundling === false ? "--no-bundle" : [],
compile ? "--compile" : [],
compileArgv
? `--compile-exec-argv=${Array.isArray(compileArgv) ? compileArgv.join(" ") : compileArgv}`
compile && typeof compile === "object" && "execArgv" in compile
? `--compile-exec-argv=${Array.isArray(compile.execArgv) ? compile.execArgv.join(" ") : compile.execArgv}`
: [],
outfile ? `--outfile=${outfile}` : `--outdir=${outdir}`,
define && Object.entries(define).map(([k, v]) => ["--define", `${k}=${v}`]),
@@ -1026,6 +1024,19 @@ function expectBundled(
if (!ESBUILD) {
const buildOutDir = useOutFile ? path.dirname(outfile!) : outdir!;
if (outfile && compile) {
if (typeof compile === "boolean" && compile) {
compile = {
outfile: outfile,
};
} else if (typeof compile === "string") {
compile = {
target: compile,
outfile: outfile,
};
}
}
const buildConfig = {
entrypoints: [...entryPaths, ...(entryPointsRaw ?? [])],
external,
@@ -1053,6 +1064,7 @@ function expectBundled(
drop,
define: define ?? {},
throw: false,
compile,
} as BuildConfig;
if (dotenv) {

View File

@@ -23,6 +23,7 @@ export const isLinux = process.platform === "linux";
export const isPosix = isMacOS || isLinux;
export const isWindows = process.platform === "win32";
export const isIntelMacOS = isMacOS && process.arch === "x64";
export const isArm64 = process.arch === "arm64";
export const isDebug = Bun.version.includes("debug");
export const isCI = process.env.CI !== undefined;
export const libcFamily: "glibc" | "musl" =
@@ -263,6 +264,24 @@ export function tempDirWithFiles(
return base;
}
class DisposableString extends String {
[Symbol.dispose]() {
fs.rmSync(this + "", { recursive: true, force: true });
}
[Symbol.asyncDispose]() {
return fs.promises.rm(this + "", { recursive: true, force: true });
}
}
export function tempDir(
basename: string,
filesOrAbsolutePathToCopyFolderFrom: DirectoryTree | string,
): DisposableString {
const base = tempDirWithFiles(basename, filesOrAbsolutePathToCopyFolderFrom);
return new DisposableString(base);
}
export function tempDirWithFilesAnon(filesOrAbsolutePathToCopyFolderFrom: DirectoryTree | string): string {
const base = tmpdirSync();
makeTreeSync(base, filesOrAbsolutePathToCopyFolderFrom);

View File

@@ -4,7 +4,7 @@
"!= alloc.ptr": 0,
"!= allocator.ptr": 0,
".arguments_old(": 279,
".stdDir()": 40,
".stdDir()": 41,
".stdFile()": 18,
"// autofix": 168,
": [^=]+= undefined,$": 260,