From 9eab12f7b8f007d248b8576cf81278491cb4dc43 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 25 Apr 2024 15:34:40 -0700 Subject: [PATCH] Support cross-compilation in `bun build --compile` (#10477) --- docs/bundler/executables.md | 107 +++++++-- src/StandaloneModuleGraph.zig | 47 ++-- src/cli.zig | 18 +- src/cli/build_command.zig | 37 ++- src/compile_target.zig | 438 ++++++++++++++++++++++++++++++++++ src/env.zig | 64 +++++ src/sys.zig | 77 +++--- 7 files changed, 710 insertions(+), 78 deletions(-) create mode 100644 src/compile_target.zig diff --git a/docs/bundler/executables.md b/docs/bundler/executables.md index 6257e5e9c855a..2e9459279dada 100644 --- a/docs/bundler/executables.md +++ b/docs/bundler/executables.md @@ -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. @@ -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()); ``` @@ -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. @@ -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. diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index 5c963a7508188..9b95856b768c1 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -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("error: failed to get temporary file name: {s}", .{@errorName(err)}); @@ -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("error: failed to get self executable path: {s}", .{@errorName(err)}); - Global.exit(1); - }; - if (comptime Environment.isWindows) { // copy self and then open it for writing @@ -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) { @@ -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]; }; @@ -518,7 +535,7 @@ pub const StandaloneModuleGraph = struct { }; if (comptime Environment.isMac) { - { + if (target.os == .mac) { var signer = std.ChildProcess.init( &.{ "codesign", diff --git a/src/cli.zig b/src/cli.zig index 9e6aec6514e4b..f21a6bbf8f753 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -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 {} @@ -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, @@ -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 = "", diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig index 76f10c47ec012..5e7fc22efb1aa 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -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 { @@ -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, @@ -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) { @@ -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(" compile {s}{s}", .{ + Output.pretty(" compile {s}{s}", .{ outfile, if (Environment.isWindows and !strings.hasSuffixComptime(outfile, ".exe")) ".exe" else "", }); + if (is_cross_compile) { + Output.pretty(" {s}\n", .{compile_target}); + } else { + Output.pretty("\n", .{}); + } + break :dump; } diff --git a/src/compile_target.zig b/src/compile_target.zig new file mode 100644 index 0000000000000..5b3756269d2f1 --- /dev/null +++ b/src/compile_target.zig @@ -0,0 +1,438 @@ +/// 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("root").bun; +const std = @import("std"); +const Environment = bun.Environment; +const strings = bun.strings; +const Output = bun.Output; +const CompileTarget = @This(); + +os: Environment.OperatingSystem = Environment.os, +arch: Environment.Archictecture = Environment.arch, +baseline: bool = !Environment.enableSIMD, +version: bun.Semver.Version = .{ + .major = @truncate(Environment.version.major), + .minor = @truncate(Environment.version.minor), + .patch = @truncate(Environment.version.patch), +}, +libc: Libc = .default, + +const Libc = enum { + /// The default libc for the target + /// "glibc" for linux, unspecified for other OSes + default, + /// musl libc + musl, + + pub fn format(self: @This(), comptime _: []const u8, _: anytype, writer: anytype) !void { + if (self == .musl) { + try writer.writeAll("-musl"); + } + } +}; + +const BaselineFormatter = struct { + baseline: bool = false, + pub fn format(self: @This(), comptime _: []const u8, _: anytype, writer: anytype) !void { + if (self.baseline) { + try writer.writeAll("-baseline"); + } + } +}; + +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; +} + +pub fn isDefault(this: *const CompileTarget) bool { + return this.eql(&.{}); +} + +pub fn toNPMRegistryURL(this: *const CompileTarget, buf: []u8) ![]const u8 { + if (bun.getenvZ("BUN_COMPILE_TARGET_TARBALL_URL")) |url| { + if (strings.hasPrefixComptime(url, "http://") or strings.hasPrefixComptime(url, "https://")) + return url; + } + + return try this.toNPMRegistryURLWithURL(buf, "https://registry.npmjs.org"); +} + +pub fn toNPMRegistryURLWithURL(this: *const CompileTarget, buf: []u8, registry_url: []const u8) ![]const u8 { + return switch (this.os) { + inline else => |os| switch (this.arch) { + inline else => |arch| 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-" ++ + os.npmName() ++ "-" ++ arch.npmName() ++ + (if (is_baseline) "-baseline" else "") ++ + "/-/bun-" ++ + os.npmName() ++ "-" ++ arch.npmName() ++ + (if (is_baseline) "-baseline" else "") ++ + "-" ++ + "{d}.{d}.{d}.tgz", .{ + registry_url, + this.version.major, + this.version.minor, + this.version.patch, + }), + }, + }, + }; +} + +pub fn format(this: @This(), comptime _: []const u8, _: anytype, writer: anytype) !void { + try std.fmt.format( + writer, + // bun-darwin-x64-baseline-v1.0.0 + // This doesn't match up 100% with npm, but that's okay. + "bun-{s}-{s}{}{}-v{d}.{d}.{d}", + .{ + this.os.npmName(), + this.arch.npmName(), + this.libc, + BaselineFormatter{ .baseline = this.baseline }, + this.version.major, + this.version.minor, + this.version.patch, + }, + ); +} + +pub fn exePath(this: *const CompileTarget, buf: *bun.PathBuffer, version_str: [:0]const u8, env: *bun.DotEnv.Loader, needs_download: *bool) [:0]const u8 { + if (this.isDefault()) brk: { + const self_exe_path = bun.selfExePath() catch break :brk; + @memcpy(buf, self_exe_path); + buf[self_exe_path.len] = 0; + needs_download.* = false; + return buf[0..self_exe_path.len :0]; + } + + if (bun.sys.existsAt(bun.toFD(std.fs.cwd()), version_str)) { + needs_download.* = false; + return version_str; + } + + const dest = bun.path.joinAbsStringBufZ( + bun.fs.FileSystem.instance.top_level_dir, + buf, + &.{ + bun.install.PackageManager.fetchCacheDirectoryPath(env).path, + version_str, + }, + .auto, + ); + + if (bun.sys.existsAt(bun.toFD(std.fs.cwd()), dest)) { + needs_download.* = false; + } + + 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 { + try HTTP.HTTPThread.init(); + var refresher = std.Progress{}; + + { + refresher.refresh(); + + // TODO: This is way too much code necessary to send a single HTTP request... + var async_http = try allocator.create(HTTP.AsyncHTTP); + 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); + { + var progress = refresher.start("Downloading", 0); + defer progress.end(); + const timeout = 30000; + const http_proxy: ?bun.URL = env.getHttpProxy(url); + + async_http.* = HTTP.AsyncHTTP.initSync( + allocator, + .GET, + url, + .{}, + "", + compressed_archive_bytes, + "", + timeout, + http_proxy, + null, + HTTP.FetchRedirect.follow, + ); + async_http.client.timeout = timeout; + async_http.client.progress_node = progress; + async_http.client.reject_unauthorized = env.getTLSRejectUnauthorized(); + + const response = try async_http.sendSync(true); + + 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); + }, + 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); + }, + 200 => {}, + else => return error.HTTPError, + } + } + + var tarball_bytes = std.ArrayListUnmanaged(u8){}; + { + refresher.refresh(); + 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); + } + + { + 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| { + node.end(); + Output.err(err, + \\Failed to decompress the downloaded tarball + \\ + \\After downloading {} from {s} + , .{ + this.*, + url_str, + }); + Global.exit(1); + }; + gunzip.readAll() catch |err| { + 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); + }; + gunzip.deinit(); + } + refresher.refresh(); + + { + var node = refresher.start("Extracting", 0); + defer node.end(); + + const libarchive = @import("./libarchive//libarchive.zig"); + var tmpname_buf: [1024]u8 = undefined; + const tempdir_name = bun.span(try bun.fs.FileSystem.instance.tmpname("tmp", &tmpname_buf, bun.fastRandom())); + var tmpdir = try std.fs.cwd().makeOpenPath(tempdir_name, .{}); + defer tmpdir.close(); + defer std.fs.cwd().deleteTree(tempdir_name) catch {}; + _ = libarchive.Archive.extractToDir( + compressed_archive_bytes.list.items, + tmpdir, + null, + void, + {}, + // "package/bin" + 2, + true, + false, + ) catch |err| { + node.end(); + Output.err(err, + \\Failed to extract the downloaded tarball + \\ + \\After downloading {} from {s} + , .{ + this.*, + url_str, + }); + Global.exit(1); + }; + + var did_retry = false; + while (true) { + bun.C.moveFileZ(bun.toFD(tmpdir), if (this.os == .windows) "bun.exe" else "bun", bun.invalid_fd, dest_z) catch |err| { + if (!did_retry) { + did_retry = true; + const dirname = bun.path.dirname(dest_z, .loose); + if (dirname.len > 0) { + std.fs.cwd().makePath(dirname) catch {}; + } + continue; + } + node.end(); + Output.err(err, "Failed to move cross-compiled bun binary into cache directory {}", .{bun.fmt.fmtPath(u8, dest_z, .{})}); + Global.exit(1); + }; + break; + } + } + refresher.refresh(); + } + } +} + +pub fn isSupported(this: *const CompileTarget) bool { + return switch (this.os) { + .windows => this.arch == .x64, + .mac => true, + .linux => this.libc == .default, + .wasm => false, + }; +} + +pub fn from(input_: []const u8) CompileTarget { + var this = CompileTarget{}; + + const input = bun.strings.trim(input_, " \t\r"); + if (input.len == 0) { + return this; + } + + var found_os = false; + var found_arch = false; + var found_baseline = false; + var found_version = false; + var found_libc = false; + + // Parse each of the supported values. + // The user shouldn't have to care about the order of the values. As long as it starts with "bun-". + // Nobody wants to remember whether its "bun-linux-x64" or "bun-x64-linux". + var splitter = bun.strings.split(input, "-"); + while (input.len > 0) { + const token = splitter.next() orelse break; + if (token.len == 0) continue; + + if (Environment.Archictecture.names.get(token)) |arch| { + this.arch = arch; + found_arch = true; + continue; + } else if (Environment.OperatingSystem.names.get(token)) |os| { + this.os = os; + found_os = true; + continue; + } else if (strings.eqlComptime(token, "modern")) { + this.baseline = false; + found_baseline = true; + continue; + } else if (strings.eqlComptime(token, "baseline")) { + this.baseline = true; + found_baseline = true; + continue; + } else if (strings.hasPrefixComptime(token, "v1.") or strings.hasPrefixComptime(token, "v0.")) { + 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); + } + + this.version = .{ + .major = version.version.major.?, + .minor = version.version.minor.?, + .patch = version.version.patch.?, + }; + found_version = true; + continue; + } + } else if (strings.eqlComptime(token, "musl")) { + this.libc = .musl; + found_libc = true; + continue; + } else { + Output.errGeneric( + \\Unsupported target {} in "bun{s}" + \\To see the supported targets: + \\ https://bun.sh/docs/bundler/executables + , + .{ + bun.fmt.quote(token), + // received input starts at "-" + input_, + }, + ); + Global.exit(1); + } + } + + 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 + this.arch = .x64; + found_arch = true; + } + + // there is no baseline arm64. + if (this.baseline and this.arch == .arm64) { + this.baseline = false; + } + + if (this.libc == .musl and this.os != .linux) { + Output.errGeneric("invalid target, musl libc only exists on linux", .{}); + Global.exit(1); + } + + if (this.arch == .wasm or this.os == .wasm) { + Output.errGeneric("invalid target, WebAssembly is not supported. Sorry!", .{}); + Global.exit(1); + } + + return this; +} + +pub fn defineValues(this: *const CompileTarget) []const []const u8 { + // Use inline else to avoid extra allocations. + switch (this.os) { + inline else => |os| switch (this.arch) { + inline .arm64, .x64 => |arch| return struct { + pub const values = &.{ + "\"" ++ os.nameString() ++ "\"", + + switch (arch) { + .x64 => "\"x64\"", + .arm64 => "\"arm64\"", + else => @compileError("TODO"), + }, + }; + }.values, + else => @panic("TODO"), + }, + } +} diff --git a/src/env.zig b/src/env.zig index d19295fd4a2ef..b7b654da1cb1e 100644 --- a/src/env.zig +++ b/src/env.zig @@ -61,6 +61,27 @@ pub const OperatingSystem = enum { // wAsM is nOt aN oPeRaTiNg SyStEm wasm, + pub const names = @import("root").bun.ComptimeStringMap( + OperatingSystem, + &.{ + .{ "windows", OperatingSystem.windows }, + .{ "win32", OperatingSystem.windows }, + .{ "win", OperatingSystem.windows }, + .{ "win64", OperatingSystem.windows }, + .{ "win_x64", OperatingSystem.windows }, + .{ "darwin", OperatingSystem.mac }, + .{ "macos", OperatingSystem.mac }, + .{ "macOS", OperatingSystem.mac }, + .{ "mac", OperatingSystem.mac }, + .{ "apple", OperatingSystem.mac }, + .{ "linux", OperatingSystem.linux }, + .{ "Linux", OperatingSystem.linux }, + .{ "linux-gnu", OperatingSystem.linux }, + .{ "gnu/linux", OperatingSystem.linux }, + .{ "wasm", OperatingSystem.wasm }, + }, + ); + /// user-facing name with capitalization pub fn displayString(self: OperatingSystem) []const u8 { return switch (self) { @@ -80,6 +101,16 @@ pub const OperatingSystem = enum { .wasm => "wasm", }; } + + /// npm package name + pub fn npmName(self: OperatingSystem) []const u8 { + return switch (self) { + .mac => "darwin", + .linux => "linux", + .windows => "windows", + .wasm => "wasm", + }; + } }; pub const os: OperatingSystem = if (isMac) @@ -92,3 +123,36 @@ else if (isWasm) OperatingSystem.wasm else @compileError("Please add your OS to the OperatingSystem enum"); + +pub const Archictecture = enum { + x64, + arm64, + wasm, + + pub fn npmName(this: Archictecture) []const u8 { + return switch (this) { + .x64 => "x64", + .arm64 => "aarch64", + .wasm => "wasm", + }; + } + + pub const names = @import("root").bun.ComptimeStringMap( + Archictecture, + &.{ + .{ "x86_64", Archictecture.x64 }, + .{ "x64", Archictecture.x64 }, + .{ "amd64", Archictecture.x64 }, + .{ "aarch64", Archictecture.arm64 }, + .{ "arm64", Archictecture.arm64 }, + .{ "wasm", Archictecture.wasm }, + }, + ); +}; + +pub const arch = if (isX64) + Archictecture.x64 +else if (isAarch64) + Archictecture.arm64 +else + @compileError("Please add your architecture to the Archictecture enum"); diff --git a/src/sys.zig b/src/sys.zig index 7de54be790b24..da7879b5c4277 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -2136,9 +2136,38 @@ pub fn exists(path: []const u8) bool { @compileError("TODO: existsOSPath"); } -pub fn directoryExistsAt(dir_: anytype, subpath: anytype) JSC.Maybe(bool) { +pub fn faccessat(dir_: anytype, subpath: anytype) JSC.Maybe(bool) { const has_sentinel = std.meta.sentinel(@TypeOf(subpath)) != null; const dir_fd = bun.toFD(dir_); + + if (comptime !has_sentinel) { + const path = std.os.toPosixPath(subpath) catch return JSC.Maybe(bool){ .err = Error.fromCode(.NAMETOOLONG, .access) }; + return faccessat(dir_fd, path); + } + + if (comptime Environment.isLinux) { + // avoid loading the libc symbol for this to reduce chances of GLIBC minimum version requirements + const rc = linux.faccessat(dir_fd.cast(), subpath, linux.F_OK, 0); + syslog("faccessat({}, {}, O_RDONLY, 0) = {d}", .{ dir_fd, bun.fmt.fmtOSPath(subpath, .{}), if (rc == 0) 0 else @intFromEnum(linux.getErrno(rc)) }); + if (rc == 0) { + return JSC.Maybe(bool){ .result = true }; + } + + return JSC.Maybe(bool){ .result = false }; + } + + // on other platforms use faccessat from libc + const rc = std.c.faccessat(dir_fd.cast(), subpath, std.os.F_OK, 0); + syslog("faccessat({}, {}, O_RDONLY, 0) = {d}", .{ dir_fd, bun.fmt.fmtOSPath(subpath, .{}), if (rc == 0) 0 else @intFromEnum(std.c.getErrno(rc)) }); + if (rc == 0) { + return JSC.Maybe(bool){ .result = true }; + } + + return JSC.Maybe(bool){ .result = false }; +} + +pub fn directoryExistsAt(dir_: anytype, subpath: anytype) JSC.Maybe(bool) { + const dir_fd = bun.toFD(dir_); if (comptime Environment.isWindows) { var wbuf: bun.WPathBuffer = undefined; const path = bun.strings.toNTPath(&wbuf, subpath); @@ -2177,40 +2206,17 @@ pub fn directoryExistsAt(dir_: anytype, subpath: anytype) JSC.Maybe(bool) { }; } - if (comptime !has_sentinel) { - const path = std.os.toPosixPath(subpath) catch return JSC.Maybe(bool){ .err = Error.fromCode(.NAMETOOLONG, .access) }; - return directoryExistsAt(dir_fd, path); - } - - if (comptime Environment.isLinux) { - // avoid loading the libc symbol for this to reduce chances of GLIBC minimum version requirements - const rc = linux.faccessat(dir_fd.cast(), subpath, linux.F_OK, 0); - syslog("faccessat({}, {}, O_DIRECTORY | O_RDONLY, 0) = {d}", .{ dir_fd, bun.fmt.fmtOSPath(subpath, .{}), if (rc == 0) 0 else @intFromEnum(linux.getErrno(rc)) }); - if (rc == 0) { - return JSC.Maybe(bool){ .result = true }; - } - - return JSC.Maybe(bool){ .result = false }; - } - - // on other platforms use faccessat from libc - const rc = std.c.faccessat(dir_fd.cast(), subpath, std.os.F_OK, 0); - syslog("faccessat({}, {}, O_DIRECTORY | O_RDONLY, 0) = {d}", .{ dir_fd, bun.fmt.fmtOSPath(subpath, .{}), if (rc == 0) 0 else @intFromEnum(std.c.getErrno(rc)) }); - if (rc == 0) { - return JSC.Maybe(bool){ .result = true }; - } - - return JSC.Maybe(bool){ .result = false }; + return faccessat(dir_fd, subpath); } -pub fn existsAt(fd: bun.FileDescriptor, subpath: []const u8) bool { +pub fn existsAt(fd: bun.FileDescriptor, subpath: [:0]const u8) bool { if (comptime Environment.isPosix) { - return system.faccessat(fd.cast(), &(std.os.toPosixPath(subpath) catch return false), 0, 0) == 0; + return faccessat(fd, subpath).result; } if (comptime Environment.isWindows) { var wbuf: bun.WPathBuffer = undefined; - const path = bun.strings.toWPath(&wbuf, subpath); + const path = bun.strings.toNTPath(&wbuf, subpath); const path_len_bytes: u16 = @truncate(path.len * 2); var nt_name = w.UNICODE_STRING{ .Length = path_len_bytes, @@ -2231,10 +2237,17 @@ pub fn existsAt(fd: bun.FileDescriptor, subpath: []const u8) bool { .SecurityQualityOfService = null, }; var basic_info: w.FILE_BASIC_INFORMATION = undefined; - return switch (kernel32.NtQueryAttributesFile(&attr, &basic_info)) { - .SUCCESS => true, - else => false, - }; + const rc = kernel32.NtQueryAttributesFile(&attr, &basic_info); + if (JSC.Maybe(bool).errnoSysP(rc, .access, subpath)) |err| { + syslog("NtQueryAttributesFile({}, O_RDONLY, 0) = {}", .{ bun.fmt.fmtOSPath(path, .{}), err }); + return false; + } + + const is_regular_file = basic_info.FileAttributes != kernel32.INVALID_FILE_ATTRIBUTES and + basic_info.FileAttributes & kernel32.FILE_ATTRIBUTE_NORMAL != 0; + syslog("NtQueryAttributesFile({}, O_RDONLY, 0) = {d}", .{ bun.fmt.fmtOSPath(path, .{}), @intFromBool(is_regular_file) }); + + return is_regular_file; } @compileError("TODO: existsAtOSPath");