Packages
Burrito is our answer to the problem of distributing Elixir applications across varied environments. Turn your Elixir application into a simple, self-contained, single-file executable for MacOS, Linux, and Windows.
Current section
Files
Jump to
Current section
Files
src/wrapper.zig
////
// DO NOT EDIT THIS FILE
////
const builtin = @import("builtin");
const launcher = @import("erlang_launcher.zig");
const build_options = @import("build_options");
const std = @import("std");
const json = std.json;
const log = std.log;
const fs = std.fs;
const Sha1 = std.crypto.hash.Sha1;
const Base64 = std.base64.url_safe_no_pad.Encoder;
// Foilz Archive Util
const foilz = @import("archiver.zig");
// Maint utils
const logger = @import("logger.zig");
const maint = @import("maintenance.zig");
const shutil = @import("shutil.zig");
// Install dir suffix
const install_suffix = ".burrito";
const plugin = @import("burrito_plugin");
const metadata = @import("metadata.zig");
const MetaStruct = metadata.MetaStruct;
const IS_LINUX = builtin.os.tag == .linux;
// Payload
pub const FOILZ_PAYLOAD = @embedFile("payload.foilz.xz");
pub const RELEASE_METADATA_JSON = @embedFile("_metadata.json");
// Memory allocator
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
var allocator = arena.allocator();
// Windows cmd argument parser
const windows = std.os.windows;
const LPCWSTR = windows.LPCWSTR;
const LPWSTR = windows.LPWSTR;
pub extern "kernel32" fn GetCommandLineW() LPWSTR;
pub extern "shell32" fn CommandLineToArgvW(lpCmdLine: LPCWSTR, out_pNumArgs: *c_int) ?[*]LPWSTR;
pub fn main() anyerror!void {
var args: ?[][]u8 = null;
// Get argvs -- on Windows we need to call CommandLineToArgvW() with GetCommandLineW()
if (builtin.os.tag == .windows) {
// Windows arguments
var arg_count: c_int = undefined;
var raw_args = CommandLineToArgvW(GetCommandLineW(), &arg_count);
var windows_arg_list = std.ArrayList([]u8).init(allocator);
var i: usize = 0;
while (i < arg_count) : (i += 1) {
const index = i;
const length = std.mem.len(raw_args.?[index]);
const argument = try std.unicode.utf16leToUtf8Alloc(allocator, raw_args.?[index][0..length]);
try windows_arg_list.append(argument);
}
args = windows_arg_list.items;
} else {
// POSIX arguments
args = try std.process.argsAlloc(allocator);
}
// If on linux, maybe install the musl libc runtime file for our pre-compiled Erlang
try maybe_install_musl_runtime();
// Trim args to only what we actually want to pass to erlang
const args_trimmed = args.?[1..];
// If this is not a production build, we always want a clean install
const wants_clean_install = !build_options.IS_PROD;
const meta = metadata.parse(allocator, RELEASE_METADATA_JSON).?;
const install_dir = (try get_install_dir(&meta))[0..];
const metadata_path = try fs.path.join(allocator, &[_][]const u8{ install_dir, "_metadata.json" });
// Check for maintenance commands
if (args_trimmed.len > 0 and std.mem.eql(u8, args_trimmed[0], "maintenance")) {
try maint.do_maint(args_trimmed[1..], install_dir);
return;
}
log.debug("Size of embedded payload is: {}", .{FOILZ_PAYLOAD.len});
log.debug("Install Directory: {s}", .{install_dir});
log.debug("Metadata path: {s}", .{metadata_path});
// Ensure the destination directory is created
try std.fs.cwd().makePath(install_dir);
// If the metadata file exists, don't install again
var needs_install: bool = false;
std.fs.accessAbsolute(metadata_path, .{}) catch |err| {
if (err == error.FileNotFound) {
needs_install = true;
} else {
log.err("We failed to open the destination directory with an unexpected error: {!}", .{err});
return;
}
};
log.debug("Passing args string: {s}", .{args_trimmed});
// Execute plugin code
plugin.burrito_plugin_entry(install_dir, RELEASE_METADATA_JSON);
// If we need an install, install the payload onto the target machine
if (needs_install or wants_clean_install) {
// If running a clean install (probably a debug build)
// delete existing install directory if it's present to prevent a MacOS SIP issue
// when "replacing" a mach-o in place
if (wants_clean_install and !needs_install) {
try fs.deleteTreeAbsolute(install_dir);
try std.fs.cwd().makePath(install_dir);
}
try do_payload_install(install_dir, metadata_path);
} else {
log.debug("Skipping archive unpacking, this machine already has the app installed!", .{});
}
// Clean up older versions
const base_install_path = try get_base_install_dir();
try maint.do_clean_old_versions(base_install_path, install_dir);
// Get Env
var env_map = try std.process.getEnvMap(allocator);
// Add _IS_TTY env variable
if (shutil.is_tty()) {
try env_map.put("_IS_TTY", "1");
} else {
try env_map.put("_IS_TTY", "0");
}
log.debug("Launching erlang...", .{});
try launcher.launch(install_dir, &env_map, &meta, args_trimmed);
}
fn do_payload_install(install_dir: []const u8, metadata_path: []const u8) !void {
// Unpack the files
try foilz.unpack_files(FOILZ_PAYLOAD, install_dir, build_options.UNCOMPRESSED_SIZE);
// Write metadata file
const file = try fs.createFileAbsolute(metadata_path, .{ .truncate = true });
try file.writeAll(RELEASE_METADATA_JSON);
}
fn get_base_install_dir() ![]const u8 {
// If we have a override for the install path, use that, otherwise, continue to return
// the standard install path
const upper_name = try std.ascii.allocUpperString(allocator, build_options.RELEASE_NAME);
const env_install_dir_name = try std.fmt.allocPrint(allocator, "{s}_INSTALL_DIR", .{upper_name});
if (std.process.getEnvVarOwned(allocator, env_install_dir_name)) |new_path| {
logger.info("Install path is being overriden using `{s}`", .{env_install_dir_name});
logger.info("New install path is: {s}", .{new_path});
return try fs.path.join(allocator, &[_][]const u8{ new_path, install_suffix });
} else |err| switch (err) {
error.InvalidWtf8 => {},
error.EnvironmentVariableNotFound => {},
error.OutOfMemory => {},
}
const app_dir = fs.getAppDataDir(allocator, install_suffix) catch {
install_dir_error();
return "";
};
return app_dir;
}
fn get_install_dir(meta: *const MetaStruct) ![]u8 {
// Combine the hash of the payload and a base dir to get a safe install directory
const base_install_path = try get_base_install_dir();
// Parse the ERTS version and app version from the metadata JSON string
const dir_name = try std.fmt.allocPrint(allocator, "{s}_erts-{s}_{s}", .{ build_options.RELEASE_NAME, meta.erts_version, meta.app_version });
// Ensure that base directory is created
std.fs.cwd().makePath(base_install_path) catch {
install_dir_error();
return "";
};
// Construct the full app install path
const name = fs.path.join(allocator, &[_][]const u8{ base_install_path, dir_name }) catch {
install_dir_error();
return "";
};
return name;
}
fn install_dir_error() void {
const upper_name = std.ascii.allocUpperString(allocator, build_options.RELEASE_NAME) catch {
return;
};
const env_install_dir_name = std.fmt.allocPrint(allocator, "{s}_INSTALL_DIR", .{upper_name}) catch {
return;
};
logger.err("We could not install this application to the default directory.", .{});
logger.err("This may be due to a permission error.", .{});
logger.err("Please override the default {s} install directory using the `{s}` environment variable.", .{ build_options.RELEASE_NAME, env_install_dir_name });
logger.err("On Linux or MacOS you can run the command: `export {s}=/some/other/path`", .{env_install_dir_name});
logger.err("On Windows you can use: `SET {s}=D:\\some\\other\\path`", .{env_install_dir_name});
std.process.exit(1);
}
fn maybe_install_musl_runtime() anyerror!void {
if (comptime IS_LINUX and !std.mem.eql(u8, build_options.MUSL_RUNTIME_PATH, "")) {
// Check if the file was already extracted
const cStr = try allocator.dupeZ(u8, build_options.MUSL_RUNTIME_PATH);
var statBuffer: std.c.Stat = undefined;
const statResult = std.c.stat(cStr, &statBuffer);
if (statResult == 0) {
// File exists
log.debug("The musl runtime file is already preset. Continuing.", .{});
return;
}
const file = std.fs.createFileAbsolute(
build_options.MUSL_RUNTIME_PATH,
.{ .read = true },
) catch |e| {
log.debug("Failed to extract burrito musl runtime: {}", .{e});
return;
};
defer file.close();
const exec_permissions = std.fs.File.PermissionsUnix.unixNew(0o754);
try file.setPermissions(.{ .inner = exec_permissions });
const MUSL_RUNTIME_BYTES = @embedFile("musl-runtime.so");
try file.writeAll(MUSL_RUNTIME_BYTES);
log.debug("Wrote musl runtime file: {s}", .{build_options.MUSL_RUNTIME_PATH});
}
}