mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
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:
66
packages/bun-types/bun.d.ts
vendored
66
packages/bun-types/bun.d.ts
vendored
@@ -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
|
||||
*
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
64
test/bundler/bun-build-compile.test.ts
Normal file
64
test/bundler/bun-build-compile.test.ts
Normal 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
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"!= alloc.ptr": 0,
|
||||
"!= allocator.ptr": 0,
|
||||
".arguments_old(": 279,
|
||||
".stdDir()": 40,
|
||||
".stdDir()": 41,
|
||||
".stdFile()": 18,
|
||||
"// autofix": 168,
|
||||
": [^=]+= undefined,$": 260,
|
||||
|
||||
Reference in New Issue
Block a user