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
burrito src wrapper.zig
Raw

src/wrapper.zig

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 Io = std.Io;
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(init: std.process.Init) !void {
const arena = init.arena.allocator();
const io = init.io;
const args = try init.minimal.args.toSlice(arena);
// Trim args to only what we actually want to pass to erlang
const args_trimmed = args[1..];
const environ = init.minimal.environ;
// If on linux, maybe install the musl libc runtime file for our pre-compiled Erlang
if (comptime IS_LINUX) try maybe_install_musl_runtime(io);
const self_path = try std.process.executablePathAlloc(io, arena);
// 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(io, environ, arena, &meta);
const metadata_path = try std.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(io, 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 Io.Dir.cwd().createDirPath(io, install_dir);
// If the metadata file exists, don't install again
var needs_install: bool = false;
Io.Dir.cwd().access(io, 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 Io.Dir.cwd().deleteTree(io, install_dir);
try Io.Dir.cwd().createDirPath(io, install_dir);
}
try do_payload_install(io, 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(io, environ, arena);
try maint.do_clean_old_versions(io, base_install_path, install_dir);
// Get Env
var env_map = try std.process.Environ.createMap(init.minimal.environ, arena);
// Add _IS_TTY env variable
if (try Io.File.stdout().isTty(io)) {
try env_map.put("_IS_TTY", "1");
} else {
try env_map.put("_IS_TTY", "0");
}
log.debug("Launching erlang...", .{});
try launcher.launch(io, install_dir, &env_map, &meta, self_path, args_trimmed);
}
fn do_payload_install(io: Io, arena: std.mem.Allocator, install_dir: []const u8, metadata_path: []const u8) !void {
// Unpack the files
try foilz.unpack_files(io, arena, FOILZ_PAYLOAD, install_dir, build_options.UNCOMPRESSED_SIZE);
// Write metadata file
const file = try Io.Dir.cwd().createFile(io, metadata_path, .{ .truncate = true });
defer file.close(io);
try file.writePositionalAll(io, RELEASE_METADATA_JSON, 0);
}
// If we have a override for the install path, use that, otherwise, continue to return
// the standard install path
fn get_base_install_dir(_: Io, environ: std.process.Environ, arena: std.mem.Allocator) ![]const u8 {
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});
var env_map = try std.process.Environ.createMap(environ, arena);
defer env_map.deinit();
if (env_map.get(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 std.fs.path.join(arena, &[_][]const u8{ new_path, install_suffix });
}
const app_dir = get_app_data_dir(arena, install_suffix) catch {
install_dir_error(arena);
return "";
};
return app_dir;
}
fn get_app_data_dir(arena: std.mem.Allocator, appname: []const u8) ![]const u8 {
const getenv = struct {
fn get(name: [*:0]const u8) ?[]const u8 {
const val = std.c.getenv(name) orelse return null;
return std.mem.sliceTo(val, 0);
}
}.get;
if (builtin.os.tag == .windows) {
const appdata = getenv("APPDATA") orelse return error.AppDataDirUnavailable;
return std.fs.path.join(arena, &.{ appdata, appname });
} else if (builtin.os.tag == .macos or builtin.os.tag.isDarwin()) {
const home = getenv("HOME") orelse return error.AppDataDirUnavailable;
return std.fs.path.join(arena, &.{ home, "Library", "Application Support", appname });
} else {
if (getenv("XDG_DATA_HOME")) |xdg| {
return std.fs.path.join(arena, &.{ xdg, appname });
}
const home = getenv("HOME") orelse return error.AppDataDirUnavailable;
return std.fs.path.join(arena, &.{ home, ".local", "share", appname });
}
}
fn get_install_dir(io: Io, environ: std.process.Environ, 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(io, environ, 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
Io.Dir.cwd().createDirPath(io, base_install_path) catch {
install_dir_error(arena);
return "";
};
// Construct the full app install path
const name = std.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(io: Io) !void {
if (!std.mem.eql(u8, build_options.MUSL_RUNTIME_PATH, "")) {
// Check if the file was already extracted using std.fs API (cross-platform)
const file_exists = Io.Dir.cwd().statFile(io, build_options.MUSL_RUNTIME_PATH, .{}) catch null;
if (file_exists != null) {
// File exists
log.debug("The musl runtime file is already present. Continuing.", .{});
return;
}
const file = Io.Dir.cwd().createFile(io, build_options.MUSL_RUNTIME_PATH, .{ .read = true }) catch |e| {
log.debug("Failed to extract burrito musl runtime: {}", .{e});
return;
};
defer file.close(io);
const exec_permissions = Io.File.Permissions.fromMode(@intCast(0o754));
try file.setPermissions(io, exec_permissions);
const MUSL_RUNTIME_BYTES = @embedFile("musl-runtime.so");
try file.writePositionalAll(io, MUSL_RUNTIME_BYTES, 0);
log.debug("Wrote musl runtime file: {s}", .{build_options.MUSL_RUNTIME_PATH});
}
}