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; 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) * @see [Bun.build API docs](https://bun.com/docs/bundler#api)
*/ */
interface BuildConfig { interface BuildConfigBase {
entrypoints: string[]; // list of file path entrypoints: string[]; // list of file path
outdir?: string; // output directory
/** /**
* @default "browser" * @default "browser"
*/ */
@@ -1671,7 +1683,6 @@ declare module "bun" {
asset?: string; asset?: string;
}; // | string; }; // | string;
root?: string; // project root root?: string; // project root
splitting?: boolean; // default true, enable code splitting
plugins?: BunPlugin[]; plugins?: BunPlugin[];
// manifest?: boolean; // whether to return manifest // manifest?: boolean; // whether to return manifest
external?: string[]; external?: string[];
@@ -1820,8 +1831,57 @@ declare module "bun" {
* ``` * ```
*/ */
tsconfig?: string; 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 * Hash and verify passwords using argon2 or bcrypt
* *

View File

@@ -55,7 +55,7 @@ pub const StandaloneModuleGraph = struct {
// by normalized file path // by normalized file path
pub fn find(this: *const StandaloneModuleGraph, name: []const u8) ?*File { pub fn find(this: *const StandaloneModuleGraph, name: []const u8) ?*File {
if (!isBunStandaloneFilePath(base_path)) { if (!isBunStandaloneFilePath(name)) {
return null; return null;
} }
@@ -348,7 +348,7 @@ pub const StandaloneModuleGraph = struct {
var entry_point_id: ?usize = null; var entry_point_id: ?usize = null;
var string_builder = bun.StringBuilder{}; var string_builder = bun.StringBuilder{};
var module_count: usize = 0; 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(output_file.dest_path);
string_builder.countZ(prefix); string_builder.countZ(prefix);
if (output_file.value == .buffer) { if (output_file.value == .buffer) {
@@ -395,7 +395,7 @@ pub const StandaloneModuleGraph = struct {
var source_map_arena = bun.ArenaAllocator.init(allocator); var source_map_arena = bun.ArenaAllocator.init(allocator);
defer source_map_arena.deinit(); defer source_map_arena.deinit();
for (output_files) |output_file| { for (output_files) |*output_file| {
if (!output_file.output_kind.isFileInStandaloneMode()) { if (!output_file.output_kind.isFileInStandaloneMode()) {
continue; continue;
} }
@@ -496,6 +496,21 @@ pub const StandaloneModuleGraph = struct {
windows_hide_console: bool = false, 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 { 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 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| { 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); cleanup(zname, fd);
Global.exit(1); Global.exit(1);
}; };
break :brk fd; break :brk fd;
}; };
@@ -821,7 +837,43 @@ pub const StandaloneModuleGraph = struct {
var needs_download: bool = true; var needs_download: bool = true;
const dest_z = target.exePath(&exe_path_buf, version_str, env, &needs_download); const dest_z = target.exePath(&exe_path_buf, version_str, env, &needs_download);
if (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); return try allocator.dupeZ(u8, dest_z);
@@ -839,27 +891,67 @@ pub const StandaloneModuleGraph = struct {
windows_hide_console: bool, windows_hide_console: bool,
windows_icon: ?[]const u8, windows_icon: ?[]const u8,
compile_exec_argv: []const u8, compile_exec_argv: []const u8,
) !void { self_exe_path: ?[]const u8,
const bytes = try toBytes(allocator, module_prefix, output_files, output_format, compile_exec_argv); ) !CompileResult {
if (bytes.len == 0) return; 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( 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| {
return CompileResult.fail(std.fmt.allocPrint(allocator, "failed to get self executable path: {s}", .{@errorName(err)}) catch "failed to get self executable path");
}
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, bytes,
if (target.isDefault()) self_exe,
bun.selfExePath() catch |err| {
Output.err(err, "failed to get self executable path", .{});
Global.exit(1);
}
else
download(allocator, target, env) catch |err| {
Output.err(err, "failed to download cross-compiled bun executable", .{});
Global.exit(1);
},
.{ .windows_hide_console = windows_hide_console }, .{ .windows_hide_console = windows_hide_console },
target, target,
); );
defer if (fd != bun.invalid_fd) fd.close();
bun.debugAssert(fd.kind == .system); 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) { if (Environment.isWindows) {
var outfile_buf: bun.OSPathBuffer = undefined; var outfile_buf: bun.OSPathBuffer = undefined;
const outfile_slice = brk: { 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| { 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); _ = bun.windows.deleteOpenedFile(fd);
if (err == error.EISDIR) {
Global.exit(1); 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.close();
fd = bun.invalid_fd;
if (windows_icon) |icon_utf8| { if (windows_icon) |icon_utf8| {
var icon_buf: bun.OSPathBuffer = undefined; var icon_buf: bun.OSPathBuffer = undefined;
const icon = bun.strings.toWPathNormalized(&icon_buf, icon_utf8); const icon = bun.strings.toWPathNormalized(&icon_buf, icon_utf8);
bun.windows.rescle.setIcon(outfile_slice, icon) catch { bun.windows.rescle.setIcon(outfile_slice, icon) catch |err| {
Output.warn("Failed to set executable icon", .{}); Output.debug("Warning: Failed to set Windows icon for executable: {s}", .{@errorName(err)});
}; };
} }
return; return .success;
} }
var buf: bun.PathBuffer = undefined; var buf: bun.PathBuffer = undefined;
const temp_location = bun.getFdPath(fd, &buf) catch |err| { 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)}); return CompileResult.fail(std.fmt.allocPrint(allocator, "failed to get path for fd: {s}", .{@errorName(err)}) catch "failed to get path for file descriptor");
Global.exit(1); };
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( bun.sys.moveFileZWithHandle(
fd, fd,
bun.FD.cwd(), bun.FD.cwd(),
bun.sliceTo(&(try std.posix.toPosixPath(temp_location)), 0), bun.sliceTo(&temp_posix, 0),
.fromStdDir(root_dir), .fromStdDir(root_dir),
bun.sliceTo(&(try std.posix.toPosixPath(std.fs.path.basename(outfile))), 0), bun.sliceTo(&outfile_posix, 0),
) catch |err| { ) catch |err| {
if (err == error.IsDir or err == error.EISDIR) { fd.close();
Output.prettyErrorln("<r><red>error<r><d>:<r> {} is a directory. Please choose a different --outfile or delete the directory", .{bun.fmt.quote(outfile)}); fd = bun.invalid_fd;
} 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)),
);
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 { pub fn fromExecutable(allocator: std.mem.Allocator) !?StandaloneModuleGraph {

View File

@@ -37,6 +37,119 @@ pub const JSBundler = struct {
env_behavior: api.DotEnvBehavior = .disable, env_behavior: api.DotEnvBehavior = .disable,
env_prefix: OwnedString = OwnedString.initEmpty(bun.default_allocator), env_prefix: OwnedString = OwnedString.initEmpty(bun.default_allocator),
tsconfig_override: 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); pub const List = bun.StringArrayHashMapUnmanaged(Config);
@@ -58,9 +171,20 @@ pub const JSBundler = struct {
errdefer if (plugins.*) |plugin| plugin.deinit(); errdefer if (plugins.*) |plugin| plugin.deinit();
var did_set_target = false; var did_set_target = false;
if (try config.getOptionalEnum(globalThis, "target", options.Target)) |target| { if (try config.getOptional(globalThis, "target", ZigString.Slice)) |slice| {
this.target = target; defer slice.deinit();
did_set_target = true; 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 // 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; 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; return this;
} }
@@ -506,6 +676,9 @@ pub const JSBundler = struct {
self.conditions.deinit(); self.conditions.deinit();
self.drop.deinit(); self.drop.deinit();
self.banner.deinit(); self.banner.deinit();
if (self.compile) |*compile| {
compile.deinit();
}
self.env_prefix.deinit(); self.env_prefix.deinit();
self.footer.deinit(); self.footer.deinit();
self.tsconfig_override.deinit(); self.tsconfig_override.deinit();
@@ -1281,6 +1454,7 @@ pub const BuildArtifact = struct {
const string = []const u8; const string = []const u8;
const CompileTarget = @import("../../compile_target.zig");
const Fs = @import("../../fs.zig"); const Fs = @import("../../fs.zig");
const resolve_path = @import("../../resolver/resolve_path.zig"); const resolve_path = @import("../../resolver/resolve_path.zig");
const std = @import("std"); 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.public_path = config.public_path.list.items;
transpiler.options.output_format = config.format; transpiler.options.output_format = config.format;
transpiler.options.bytecode = config.bytecode; transpiler.options.bytecode = config.bytecode;
transpiler.options.compile = config.compile != null;
transpiler.options.output_dir = config.outdir.slice(); transpiler.options.output_dir = config.outdir.slice();
transpiler.options.root_dir = config.rootdir.slice(); transpiler.options.root_dir = config.rootdir.slice();
@@ -1782,6 +1783,114 @@ pub const BundleV2 = struct {
bun.destroy(this); 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 { pub fn onComplete(this: *JSBundleCompletionTask) void {
var globalThis = this.globalThis; var globalThis = this.globalThis;
defer this.deref(); defer this.deref();
@@ -1799,33 +1908,25 @@ pub const BundleV2 = struct {
const promise = this.promise.swap(); const promise = this.promise.swap();
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 };
}
}
}
switch (this.result) { switch (this.result) {
.pending => unreachable, .pending => unreachable,
.err => brk: { .err => this.toJSError(promise, globalThis),
if (this.config.throw_on_error) {
promise.reject(globalThis, this.log.toJSAggregateError(globalThis, bun.String.static("Bundle failed")));
break :brk;
}
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);
},
.value => |*build| { .value => |*build| {
const root_obj = jsc.JSValue.createEmptyObject(globalThis, 3); 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); const output_files_js = jsc.JSValue.createEmptyArray(globalThis, output_files.len) catch return promise.reject(globalThis, error.JSError);
if (output_files_js == .zero) { if (output_files_js == .zero) {
@panic("Unexpected pending JavaScript exception in JSBundleCompletionTask.onComplete. This is a bug in Bun."); @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( bun.default_allocator.dupe(
u8, u8,
bun.path.joinAbsString( 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 }, &[_]string{ this.config.dir.slice(), this.config.outdir.slice(), output_file.dest_path },
.auto, .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() }; 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(); 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); try c.writeOutputFilesToDisk(root_path, chunks, &output_files);
} else { } else {
// In-memory build // In-memory build

View File

@@ -1,10 +1,4 @@
pub const BuildCommand = struct { 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 { pub fn exec(ctx: Command.Context, fetcher: ?*BundleV2.DependenciesScanner) !void {
Global.configureAllocator(.{ .long_running = true }); Global.configureAllocator(.{ .long_running = true });
const allocator = ctx.allocator; const allocator = ctx.allocator;
@@ -26,7 +20,9 @@ pub const BuildCommand = struct {
const compile_target = &ctx.bundler_options.compile_target; const compile_target = &ctx.bundler_options.compile_target;
if (ctx.bundler_options.compile) { if (ctx.bundler_options.compile) {
const compile_define_keys = compile_target.defineKeys();
const compile_define_values = compile_target.defineValues(); const compile_define_values = compile_target.defineValues();
if (ctx.args.define) |*define| { if (ctx.args.define) |*define| {
var keys = try std.ArrayList(string).initCapacity(bun.default_allocator, compile_define_keys.len + define.keys.len); var keys = try std.ArrayList(string).initCapacity(bun.default_allocator, compile_define_keys.len + define.keys.len);
keys.appendSliceAssumeCapacity(compile_define_keys); keys.appendSliceAssumeCapacity(compile_define_keys);
@@ -426,7 +422,7 @@ pub const BuildCommand = struct {
} }
} }
try bun.StandaloneModuleGraph.toExecutable( const result = bun.StandaloneModuleGraph.toExecutable(
compile_target, compile_target,
allocator, allocator,
output_files, output_files,
@@ -438,7 +434,17 @@ pub const BuildCommand = struct {
ctx.bundler_options.windows_hide_console, ctx.bundler_options.windows_hide_console,
ctx.bundler_options.windows_icon, ctx.bundler_options.windows_icon,
ctx.bundler_options.compile_exec_argv orelse "", 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 = @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) { const compiled_elapsed_digit_count: isize = switch (compiled_elapsed) {
0...9 => 3, 0...9 => 3,

View File

@@ -1,15 +1,10 @@
const CompileTarget = @This();
/// Used for `bun build --compile` /// Used for `bun build --compile`
/// ///
/// This downloads and extracts the bun binary for the target platform /// This downloads and extracts the bun binary for the target platform
/// It uses npm to download the bun binary from the npm registry /// It uses npm to download the bun binary from the npm registry
/// It stores the downloaded binary into the bun install cache. /// It stores the downloaded binary into the bun install cache.
/// ///
const bun = @import("bun"); const CompileTarget = @This();
const Environment = bun.Environment;
const strings = bun.strings;
const Output = bun.Output;
os: Environment.OperatingSystem = Environment.os, os: Environment.OperatingSystem = Environment.os,
arch: Environment.Architecture = Environment.arch, 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 { 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; 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 { 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) { return switch (this.os) {
inline else => |os| switch (this.arch) { inline else => |os| switch (this.arch) {
inline else => |arch| switch (this.libc) { inline else => |arch| switch (this.libc) {
inline else => |libc| switch (this.baseline) { inline else => |libc| switch (this.baseline) {
// https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-0.1.6.tgz // 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() ++ os.npmName() ++ "-" ++ arch.npmName() ++
libc.npmName() ++ libc.npmName() ++
(if (is_baseline) "-baseline" else "") ++ (if (is_baseline) "-baseline" else "") ++
@@ -89,7 +99,13 @@ pub fn toNPMRegistryURLWithURL(this: *const CompileTarget, buf: []u8, registry_u
this.version.major, this.version.major,
this.version.minor, this.version.minor,
this.version.patch, 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; 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 { pub fn downloadToPath(this: *const CompileTarget, env: *bun.DotEnv.Loader, allocator: std.mem.Allocator, dest_z: [:0]const u8) !void {
HTTP.HTTPThread.init(&.{}); HTTP.HTTPThread.init(&.{});
var refresher = bun.Progress{}; 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); var compressed_archive_bytes = try allocator.create(MutableString);
compressed_archive_bytes.* = try MutableString.init(allocator, 24 * 1024 * 1024); compressed_archive_bytes.* = try MutableString.init(allocator, 24 * 1024 * 1024);
var url_buffer: [2048]u8 = undefined; var url_buffer: [2048]u8 = undefined;
const url_str = try bun.default_allocator.dupe(u8, try this.toNPMRegistryURL(&url_buffer)); const url_str = this.toNPMRegistryURL(&url_buffer) catch |err| {
const url = bun.URL.parse(url_str); // 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); var progress = refresher.start("Downloading", 0);
defer progress.end(); defer progress.end();
@@ -186,30 +203,15 @@ pub fn downloadToPath(this: *const CompileTarget, env: *bun.DotEnv.Loader, alloc
switch (response.status_code) { switch (response.status_code) {
404 => { 404 => {
Output.errGeneric( // Return error without printing - let caller handle the messaging
\\Does this target and version of Bun exist? return error.TargetNotFound;
\\
\\404 downloading {} from {s}
, .{
this.*,
url_str,
});
Global.exit(1);
}, },
403, 429, 499...599 => |status| { 403, 429, 499...599 => {
Output.errGeneric( // Return error without printing - let caller handle the messaging
\\Failed to download cross-compilation target. return error.NetworkError;
\\
\\HTTP {d} downloading {} from {s}
, .{
status,
this.*,
url_str,
});
Global.exit(1);
}, },
200 => {}, 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); defer compressed_archive_bytes.list.deinit(allocator);
if (compressed_archive_bytes.list.items.len == 0) { if (compressed_archive_bytes.list.items.len == 0) {
Output.errGeneric( // Return error without printing - let caller handle the messaging
\\Failed to verify the integrity of the downloaded tarball. return error.InvalidResponse;
\\
\\Received empty content downloading {} from {s}
, .{
this.*,
url_str,
});
Global.exit(1);
} }
{ {
var node = refresher.start("Decompressing", 0); var node = refresher.start("Decompressing", 0);
defer node.end(); 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(); node.end();
Output.err(err, // Return error without printing - let caller handle the messaging
\\Failed to decompress the downloaded tarball return error.InvalidResponse;
\\
\\After downloading {} from {s}
, .{
this.*,
url_str,
});
Global.exit(1);
}; };
gunzip.readAll() catch |err| { gunzip.readAll() catch {
node.end(); node.end();
// One word difference so if someone reports the bug we can tell if it happened in init or readAll. // Return error without printing - let caller handle the messaging
Output.err(err, return error.InvalidResponse;
\\Failed to deflate the downloaded tarball
\\
\\After downloading {} from {s}
, .{
this.*,
url_str,
});
Global.exit(1);
}; };
gunzip.deinit(); gunzip.deinit();
} }
@@ -282,22 +262,15 @@ pub fn downloadToPath(this: *const CompileTarget, env: *bun.DotEnv.Loader, alloc
// "package/bin" // "package/bin"
.depth_to_skip = 2, .depth_to_skip = 2,
}, },
) catch |err| { ) catch {
node.end(); node.end();
Output.err(err, // Return error without printing - let caller handle the messaging
\\Failed to extract the downloaded tarball return error.ExtractionFailed;
\\
\\After downloading {} from {s}
, .{
this.*,
url_str,
});
Global.exit(1);
}; };
var did_retry = false; var did_retry = false;
while (true) { 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) { if (!did_retry) {
did_retry = true; did_retry = true;
const dirname = bun.path.dirname(dest_z, .loose); 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 // fallthrough, failed for another reason
} }
node.end(); node.end();
Output.err(err, "Failed to move cross-compiled bun binary into cache directory {}", .{bun.fmt.fmtPath(u8, dest_z, .{})}); // Return error without printing - let caller handle the messaging
Global.exit(1); return error.ExtractionFailed;
}; };
break; break;
} }
@@ -331,9 +304,13 @@ pub fn isSupported(this: *const CompileTarget) bool {
}; };
} }
pub fn from(input_: []const u8) CompileTarget { pub const ParseError = error{
var this = CompileTarget{}; UnsupportedTarget,
InvalidTarget,
};
pub fn tryFrom(input_: []const u8) ParseError!CompileTarget {
var this = CompileTarget{};
const input = bun.strings.trim(input_, " \t\r"); const input = bun.strings.trim(input_, " \t\r");
if (input.len == 0) { if (input.len == 0) {
return this; 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..])); const version = bun.Semver.Version.parse(bun.Semver.SlicedString.init(token[1..], token[1..]));
if (version.valid) { if (version.valid) {
if (version.version.major == null or version.version.minor == null or version.version.patch == null) { 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, .{}); return error.InvalidTarget;
Global.exit(1);
} }
this.version = .{ this.version = .{
@@ -390,21 +366,15 @@ pub fn from(input_: []const u8) CompileTarget {
found_libc = true; found_libc = true;
continue; continue;
} else { } else {
Output.errGeneric( return error.UnsupportedTarget;
\\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);
} }
} }
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) { if (found_os and !found_arch) {
// default to x64 if no arch is specified but OS is specified // 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 // 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) { if (this.libc == .musl and this.os != .linux) {
Output.errGeneric("invalid target, musl libc only exists on linux", .{}); return error.InvalidTarget;
Global.exit(1);
} }
if (this.arch == .wasm or this.os == .wasm) { if (this.arch == .wasm or this.os == .wasm) {
Output.errGeneric("invalid target, WebAssembly is not supported. Sorry!", .{}); return error.InvalidTarget;
Global.exit(1);
} }
return this; 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 { pub fn defineValues(this: *const CompileTarget) []const []const u8 {
// Use inline else to avoid extra allocations. // Use inline else to avoid extra allocations.
switch (this.os) { 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 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", { itBundled("compile/HelloWorldWithProcessVersionsBun", {
compile: true, compile: true,
files: { files: {
[`/${process.platform}-${process.arch}.js`]: "module.exports = process.versions.bun;",
"/entry.ts": /* js */ ` "/entry.ts": /* js */ `
process.exitCode = 1; process.exitCode = 1;
process.versions.bun = "bun!"; process.versions.bun = "bun!";
@@ -26,9 +25,49 @@ describe("bundler", () => {
process.exitCode = 0; process.exitCode = 0;
} }
`, `,
[`/${process.platform}-${process.arch}.js`]: "module.exports = process.versions.bun;",
}, },
run: { exitCode: 0 }, 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", { itBundled("compile/HelloWorldBytecode", {
compile: true, compile: true,
bytecode: true, bytecode: true,

View File

@@ -2,10 +2,12 @@ import { describe } from "bun:test";
import { itBundled } from "./expectBundled"; import { itBundled } from "./expectBundled";
describe("bundler", () => { describe("bundler", () => {
itBundled("compile/HTMLServerBasic", { for (const backend of ["api", "cli"] as const) {
compile: true, itBundled(`compile/${backend}/HTMLServerBasic`, {
files: { compile: true,
"/entry.ts": /* js */ ` backend: backend,
files: {
"/entry.ts": /* js */ `
import index from "./index.html"; import index from "./index.html";
using server = Bun.serve({ using server = Bun.serve({
@@ -24,7 +26,7 @@ describe("bundler", () => {
console.log("Has h1:", html.includes("Hello HTML")); console.log("Has h1:", html.includes("Hello HTML"));
`, `,
"/index.html": /* html */ ` "/index.html": /* html */ `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@@ -37,24 +39,25 @@ describe("bundler", () => {
</body> </body>
</html> </html>
`, `,
"/styles.css": /* css */ ` "/styles.css": /* css */ `
body { body {
background: blue; background: blue;
} }
`, `,
"/app.js": /* js */ ` "/app.js": /* js */ `
console.log("Client app loaded"); console.log("Client app loaded");
`, `,
}, },
run: { run: {
stdout: "Status: 200\nContent-Type: text/html;charset=utf-8\nHas HTML tag: true\nHas h1: true", stdout: "Status: 200\nContent-Type: text/html;charset=utf-8\nHas HTML tag: true\nHas h1: true",
}, },
}); });
itBundled("compile/HTMLServerMultipleRoutes", { itBundled(`compile/${backend}/HTMLServerMultipleRoutes`, {
compile: true, compile: true,
files: { backend: backend,
"/entry.ts": /* js */ ` files: {
"/entry.ts": /* js */ `
import home from "./home.html"; import home from "./home.html";
import about from "./about.html"; import about from "./about.html";
@@ -78,7 +81,7 @@ describe("bundler", () => {
const aboutHtml = await aboutRes.text(); const aboutHtml = await aboutRes.text();
console.log("About has content:", aboutHtml.includes("About Page")); console.log("About has content:", aboutHtml.includes("About Page"));
`, `,
"/home.html": /* html */ ` "/home.html": /* html */ `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@@ -91,7 +94,7 @@ describe("bundler", () => {
</body> </body>
</html> </html>
`, `,
"/about.html": /* html */ ` "/about.html": /* html */ `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@@ -104,18 +107,19 @@ describe("bundler", () => {
</body> </body>
</html> </html>
`, `,
"/styles.css": /* css */ ` "/styles.css": /* css */ `
body { body {
margin: 0; margin: 0;
font-family: sans-serif; font-family: sans-serif;
} }
`, `,
"/app.js": /* js */ ` "/app.js": /* js */ `
console.log("App loaded"); console.log("App loaded");
`, `,
}, },
run: { run: {
stdout: "Home status: 200\nHome has content: true\nAbout status: 200\nAbout has content: true", 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", () => { describe("bundler", () => {
// Test that the --compile-exec-argv flag works for both runtime processing and execArgv // Test that the --compile-exec-argv flag works for both runtime processing and execArgv
itBundled("compile/CompileExecArgvDualBehavior", { itBundled("compile/CompileExecArgvDualBehavior", {
compile: true, compile: {
compileArgv: "--title=CompileExecArgvDualBehavior --smol", execArgv: ["--title=CompileExecArgvDualBehavior", "--smol"],
},
files: { files: {
"/entry.ts": /* js */ ` "/entry.ts": /* js */ `
// Test that --compile-exec-argv both processes flags AND populates execArgv // Test that --compile-exec-argv both processes flags AND populates execArgv

View File

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

View File

@@ -23,6 +23,7 @@ export const isLinux = process.platform === "linux";
export const isPosix = isMacOS || isLinux; export const isPosix = isMacOS || isLinux;
export const isWindows = process.platform === "win32"; export const isWindows = process.platform === "win32";
export const isIntelMacOS = isMacOS && process.arch === "x64"; export const isIntelMacOS = isMacOS && process.arch === "x64";
export const isArm64 = process.arch === "arm64";
export const isDebug = Bun.version.includes("debug"); export const isDebug = Bun.version.includes("debug");
export const isCI = process.env.CI !== undefined; export const isCI = process.env.CI !== undefined;
export const libcFamily: "glibc" | "musl" = export const libcFamily: "glibc" | "musl" =
@@ -263,6 +264,24 @@ export function tempDirWithFiles(
return base; 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 { export function tempDirWithFilesAnon(filesOrAbsolutePathToCopyFolderFrom: DirectoryTree | string): string {
const base = tmpdirSync(); const base = tmpdirSync();
makeTreeSync(base, filesOrAbsolutePathToCopyFolderFrom); makeTreeSync(base, filesOrAbsolutePathToCopyFolderFrom);

View File

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