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");
// 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");
// Windows cmd argument parser
const windows = std.os.windows;
const LPCWSTR = windows.LPCWSTR;
const LPWSTR = windows.LPWSTR;
pub fn main() !void {
var arena_impl = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena_impl.deinit();
const arena = arena_impl.allocator();
const args = try std.process.argsAlloc(arena);
// If on linux, maybe install the musl libc runtime file for our pre-compiled Erlang
try maybe_install_musl_runtime(arena);
// Trim args to only what we actually want to pass to erlang
const self_path = try std.fs.selfExePathAlloc(arena);
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(arena, RELEASE_METADATA_JSON).?;
const install_dir = try get_install_dir(arena, &meta);
const metadata_path = try fs.path.join(arena, &.{ 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: {t}", .{err});
return;
}
};
log.debug("Passing args string: {any}", .{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(arena, 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(arena);
try maint.do_clean_old_versions(base_install_path, install_dir);
// Get Env
var env_map = try std.process.getEnvMap(arena);
// Add _IS_TTY env variable
if (std.fs.File.stdout().isTty()) {
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, self_path, args_trimmed);
}
fn do_payload_install(arena: std.mem.Allocator, install_dir: []const u8, metadata_path: []const u8) !void {
// Unpack the files
try foilz.unpack_files(arena, 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(arena: std.mem.Allocator) ![]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(arena, build_options.RELEASE_NAME);
const env_install_dir_name = try std.fmt.allocPrint(arena, "{s}_INSTALL_DIR", .{upper_name});
if (std.process.getEnvVarOwned(arena, env_install_dir_name)) |new_path| {
logger.info("Install path is being overridden using `{s}`", .{env_install_dir_name});
logger.info("New install path is: {s}", .{new_path});
return try fs.path.join(arena, &[_][]const u8{ new_path, install_suffix });
} else |err| switch (err) {
error.InvalidWtf8 => {},
error.EnvironmentVariableNotFound => {},
error.OutOfMemory => {},
}
const app_dir = fs.getAppDataDir(arena, install_suffix) catch {
install_dir_error(arena);
return "";
};
return app_dir;
}
fn get_install_dir(arena: std.mem.Allocator, 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(arena);
// Parse the ERTS version and app version from the metadata JSON string
const dir_name = try std.fmt.allocPrint(
arena,
"{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(arena);
return "";
};
// Construct the full app install path
const name = fs.path.join(arena, &.{ base_install_path, dir_name }) catch {
install_dir_error(arena);
return "";
};
return name;
}
fn install_dir_error(arena: std.mem.Allocator) void {
const upper_name = std.ascii.allocUpperString(arena, build_options.RELEASE_NAME) catch {
return;
};
const env_install_dir_name = std.fmt.allocPrint(arena, "{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(arena: std.mem.Allocator) !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 arena.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});
}
}