Skip to content

Commit

Permalink
Support cross-compilation in bun build --compile (#10477)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jarred-Sumner authored Apr 25, 2024
1 parent 196cc2a commit 9eab12f
Show file tree
Hide file tree
Showing 7 changed files with 710 additions and 78 deletions.
107 changes: 90 additions & 17 deletions docs/bundler/executables.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,76 @@ Hello world!

All imported files and packages are bundled into the executable, along with a copy of the Bun runtime. All built-in Bun and Node.js APIs are supported.

{% callout %}
## Cross-compile to other platforms

**Note** — Currently, the `--compile` flag can only accept a single entrypoint at a time and does not support the following flags:
The `--target` flag lets you compile your standalone executable for a different operating system, architecture, or version of Bun than the machine you're running `bun build` on.

- `--outdir` — use `outfile` instead.
- `--splitting`
- `--public-path`
To build for Linux x64 (most servers):

```sh
bun build --compile --target=bun-linux-x64 ./index.ts --outfile myapp

# To support CPUs from before 2013, use the baseline version (nehalem)
bun build --compile --target=bun-linux-x64-baseline ./index.ts --outfile myapp

# To explicitly only support CPUs from 2013 and later, use the modern version (haswell)
# modern is faster, but baseline is more compatible.
bun build --compile --target=bun-linux-x64-modern ./index.ts --outfile myapp
```

To build for Linux ARM64 (e.g. Graviton or Raspberry Pi):

```sh
# Note: the default architecture is x64 if no architecture is specified.
bun build --compile --target=bun-linux-arm64 ./index.ts --outfile myapp
```

To build for Windows x64:

```sh
bun build --compile --target=bun-windows-x64 ./path/to/my/app.ts --outfile myapp

# To support CPUs from before 2013, use the baseline version (nehalem)
bun build --compile --target=bun-windows-x64-baseline ./path/to/my/app.ts --outfile myapp

# To explicitly only support CPUs from 2013 and later, use the modern version (haswell)
bun build --compile --target=bun-windows-x64-modern ./path/to/my/app.ts --outfile myapp

# note: if no .exe extension is provided, Bun will automatically add it for Windows executables
```

To build for macOS arm64:

```sh
bun build --compile --target=bun-darwin-arm64 ./path/to/my/app.ts --outfile myapp
```

{% /callout %}
To build for macOS x64:

```sh
bun build --compile --target=bun-darwin-x64 ./path/to/my/app.ts --outfile myapp
```

#### Supported targets

The order of the `--target` flag does not matter, as long as they're delimited by a `-`.

| --target | Operating System | Architecture | Modern | Baseline |
| --------------------- | ---------------- | ------------ | ------ | -------- |
| bun-linux-x64 | Linux | x64 |||
| bun-linux-arm64 | Linux | arm64 || N/A |
| bun-windows-x64 | Windows | x64 |||
| ~~bun-windows-arm64~~ | Windows | arm64 |||
| bun-darwin-x64 | macOS | x64 |||
| bun-darwin-arm64 | macOS | arm64 || N/A |

On x64 platforms, Bun uses SIMD optimizations which require a modern CPU supporting AVX2 instructions. The `-baseline` build of Bun is for older CPUs that don't support these optimizations. Normally, when you install Bun we automatically detect which version to use but this can be harder to do when cross-compiling since you might not know the target CPU. You usually don't need to worry about it on Darwin x64, but it is relevant for Windows x64 and Linux x64. If you or your users see `"Illegal instruction"` errors, you might need to use the baseline version.

## Deploying to production

Compiled executables reduce memory usage and improve Bun's start time.

Normally, Bun reads and transpiles JavaScript and TypeScript files on `import` and `require`. This is part of what makes so much of Bun "just work", but it's not free. It costs time and memory to read files from disk, resolve file paths, parse, transpile, and print source code.
Normally, Bun reads and transpiles JavaScript and TypeScript files on `import` and `require`. This is part of what makes so much of Bun "just work", but it's not free. It costs time and memory to read files from disk, resolve file paths, parse, transpile, and print source code.

With compiled executables, you can move that cost from runtime to build-time.

Expand All @@ -58,7 +113,7 @@ You can use `bun:sqlite` imports with `bun build --compile`.
By default, the database is resolved relative to the current working directory of the process.

```js
import db from './my.db' with {type: "sqlite"};
import db from "./my.db" with { type: "sqlite" };

console.log(db.query("select * from users LIMIT 1").get());
```
Expand All @@ -70,42 +125,49 @@ $ cd /home/me/Desktop
$ ./hello
```

## Embedding files
## Embed assets & files

Standalone executables support embedding files.

To embed files into an executable with `bun build --compile`, import the file in your code

```js
```ts
// this becomes an internal file path
import icon from "./icon.png";

import icon from "./icon.png" with { type: "file" };
import { file } from "bun";

export default {
fetch(req) {
// Embedded files can be streamed from Response objects
return new Response(file(icon));
},
};
```

You may need to specify a `--loader` for it to be treated as a `"file"` loader (so you get back a file path).

Embedded files can be read using `Bun.file`'s functions or the Node.js `fs.readFile` function (in `"node:fs"`).

### Embedding SQLite databases
For example, to read the contents of the embedded file:

```js
import icon from "./icon.png" with { type: "file" };
import { file } from "bun";

const bytes = await file(icon).arrayBuffer();
```

### Embed SQLite databases

If your application wants to embed a SQLite database, set `type: "sqlite"` in the import attribute and the `embed` attribute to `"true"`.

```js
import myEmbeddedDb from "./my.db" with {type: "sqlite", embed: "true"};
import myEmbeddedDb from "./my.db" with { type: "sqlite", embed: "true" };

console.log(myEmbeddedDb.query("select * from users LIMIT 1").get());
```

This database is read-write, but all changes are lost when the executable exits (since it's stored in memory).

### Embedding N-API Addons
### Embed N-API Addons

As of Bun v1.0.23, you can embed `.node` files into executables.

Expand All @@ -120,3 +182,14 @@ Unfortunately, if you're using `@mapbox/node-pre-gyp` or other similar tools, yo
## Minification

To trim down the size of the executable a little, pass `--minify` to `bun build --compile`. This uses Bun's minifier to reduce the code size. Overall though, Bun's binary is still way too big and we need to make it smaller.

## Unsupported CLI arguments

Currently, the `--compile` flag can only accept a single entrypoint at a time and does not support the following flags:

- `--outdir` — use `outfile` instead.
- `--splitting`
- `--public-path`
- `--target=node` or `--target=browser`
- `--format` - always outputs a binary executable. Internally, it's almost esm.
- `--no-bundle` - we always bundle everything into the executable.
47 changes: 32 additions & 15 deletions src/StandaloneModuleGraph.zig
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ pub const StandaloneModuleGraph = struct {
else
std.mem.page_size;

pub fn inject(bytes: []const u8) bun.FileDescriptor {
pub fn inject(bytes: []const u8, self_exe: [:0]const u8) bun.FileDescriptor {
var buf: [bun.MAX_PATH_BYTES]u8 = undefined;
var zname: [:0]const u8 = bun.span(bun.fs.FileSystem.instance.tmpname("bun-build", &buf, @as(u64, @bitCast(std.time.milliTimestamp()))) catch |err| {
Output.prettyErrorln("<r><red>error<r><d>:<r> failed to get temporary file name: {s}", .{@errorName(err)});
Expand All @@ -272,11 +272,6 @@ pub const StandaloneModuleGraph = struct {
}.toClean;

const cloned_executable_fd: bun.FileDescriptor = brk: {
const self_exe = bun.selfExePath() catch |err| {
Output.prettyErrorln("<r><red>error<r><d>:<r> failed to get self executable path: {s}", .{@errorName(err)});
Global.exit(1);
};

if (comptime Environment.isWindows) {
// copy self and then open it for writing

Expand Down Expand Up @@ -467,17 +462,46 @@ pub const StandaloneModuleGraph = struct {
return cloned_executable_fd;
}

pub const CompileTarget = @import("./compile_target.zig");

pub fn download(allocator: std.mem.Allocator, target: *const CompileTarget, env: *bun.DotEnv.Loader) ![:0]const u8 {
var exe_path_buf: bun.PathBuffer = undefined;
var version_str_buf: [1024]u8 = undefined;
const version_str = try std.fmt.bufPrintZ(&version_str_buf, "{}", .{target});
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);
}

return try allocator.dupeZ(u8, dest_z);
}

pub fn toExecutable(
target: *const CompileTarget,
allocator: std.mem.Allocator,
output_files: []const bun.options.OutputFile,
root_dir: std.fs.Dir,
module_prefix: []const u8,
outfile: []const u8,
env: *bun.DotEnv.Loader,
) !void {
const bytes = try toBytes(allocator, module_prefix, output_files);
if (bytes.len == 0) return;

const fd = inject(bytes);
const fd = inject(
bytes,
if (target.isDefault())
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);
},
);
fd.assertKind(.system);

if (Environment.isWindows) {
Expand All @@ -486,13 +510,6 @@ pub const StandaloneModuleGraph = struct {
const outfile_w = bun.strings.toWPathNormalized(&outfile_buf, std.fs.path.basenameWindows(outfile));
bun.assert(outfile_w.ptr == &outfile_buf);
const outfile_buf_u16 = bun.reinterpretSlice(u16, &outfile_buf);
if (!bun.strings.endsWithComptime(outfile, ".exe")) {
// append .exe
const suffix = comptime bun.strings.w(".exe");
@memcpy(outfile_buf_u16[outfile_w.len..][0..suffix.len], suffix);
outfile_buf_u16[outfile_w.len + suffix.len] = 0;
break :brk outfile_buf_u16[0 .. outfile_w.len + suffix.len :0];
}
outfile_buf_u16[outfile_w.len] = 0;
break :brk outfile_buf_u16[0..outfile_w.len :0];
};
Expand All @@ -518,7 +535,7 @@ pub const StandaloneModuleGraph = struct {
};

if (comptime Environment.isMac) {
{
if (target.os == .mac) {
var signer = std.ChildProcess.init(
&.{
"codesign",
Expand Down
18 changes: 17 additions & 1 deletion src/cli.zig
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub var start_time: i128 = undefined;
const Bunfig = @import("./bunfig.zig").Bunfig;

pub const Cli = struct {
pub const CompileTarget = @import("./compile_target.zig");
var wait_group: sync.WaitGroup = undefined;
pub var log_: logger.Log = undefined;
pub fn startTransform(_: std.mem.Allocator, _: Api.TransformOptions, _: *logger.Log) anyerror!void {}
Expand Down Expand Up @@ -674,7 +675,21 @@ pub const Arguments = struct {
}

const TargetMatcher = strings.ExactSizeMatcher(8);
if (args.option("--target")) |_target| {
if (args.option("--target")) |_target| brk: {
if (comptime cmd == .BuildCommand) {
if (args.flag("--compile")) {
if (_target.len > 4 and strings.hasPrefixComptime(_target, "bun-")) {
ctx.bundler_options.compile_target = Cli.CompileTarget.from(_target[3..]);
if (!ctx.bundler_options.compile_target.isSupported()) {
Output.errGeneric("Unsupported compile target: {}\n", .{ctx.bundler_options.compile_target});
Global.exit(1);
}
opts.target = .bun;
break :brk;
}
}
}

opts.target = opts.target orelse switch (TargetMatcher.match(_target)) {
TargetMatcher.case("browser") => Api.Target.browser,
TargetMatcher.case("node") => Api.Target.node,
Expand Down Expand Up @@ -1179,6 +1194,7 @@ pub const Command = struct {

pub const BundlerOptions = struct {
compile: bool = false,
compile_target: Cli.CompileTarget = .{},

outdir: []const u8 = "",
outfile: []const u8 = "",
Expand Down
37 changes: 24 additions & 13 deletions src/cli/build_command.zig
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,6 @@ pub const BuildCommand = struct {
"process.arch",
};

const compile_define_values = &.{
"\"" ++ Environment.os.nameString() ++ "\"",

switch (@import("builtin").target.cpu.arch) {
.x86_64 => "\"x64\"",
.aarch64 => "\"arm64\"",
else => @compileError("TODO"),
},
};

pub fn exec(
ctx: Command.Context,
) !void {
Expand All @@ -62,7 +52,10 @@ pub const BuildCommand = struct {
ctx.args.target = .bun;
}

const compile_target = &ctx.bundler_options.compile_target;

if (ctx.bundler_options.compile) {
const compile_define_values = compile_target.defineValues();
if (ctx.args.define == null) {
ctx.args.define = .{
.keys = compile_define_keys,
Expand Down Expand Up @@ -386,12 +379,24 @@ pub const BuildCommand = struct {

Output.flush();

const is_cross_compile = !compile_target.isDefault();

if (outfile.len == 0 or strings.eqlComptime(outfile, ".") or strings.eqlComptime(outfile, "..") or strings.eqlComptime(outfile, "../")) {
outfile = "index";
}

if (compile_target.os == .windows and !strings.hasSuffixComptime(outfile, ".exe")) {
outfile = try std.fmt.allocPrint(allocator, "{s}.exe", .{outfile});
}

try bun.StandaloneModuleGraph.toExecutable(
compile_target,
allocator,
output_files,
root_dir,
this_bundler.options.public_path,
outfile,
this_bundler.env,
);
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) {
Expand All @@ -402,16 +407,22 @@ pub const BuildCommand = struct {
else => 0,
};
const padding_buf = [_]u8{' '} ** 16;

Output.pretty("{s}", .{padding_buf[0..@as(usize, @intCast(compiled_elapsed_digit_count))]});
const padding_ = padding_buf[0..@as(usize, @intCast(compiled_elapsed_digit_count))];
Output.pretty("{s}", .{padding_});

Output.printElapsedStdoutTrim(@as(f64, @floatFromInt(compiled_elapsed)));

Output.prettyln(" <green>compile<r> <b><blue>{s}{s}<r>", .{
Output.pretty(" <green>compile<r> <b><blue>{s}{s}<r>", .{
outfile,
if (Environment.isWindows and !strings.hasSuffixComptime(outfile, ".exe")) ".exe" else "",
});

if (is_cross_compile) {
Output.pretty(" <r><d>{s}<r>\n", .{compile_target});
} else {
Output.pretty("\n", .{});
}

break :dump;
}

Expand Down
Loading

0 comments on commit 9eab12f

Please sign in to comment.