Burrito NG 🌯
View SourceBurrito NG is a maintained fork of Burrito, maintained by MUDROCK SOFTWARE LTD. It is not compatible with the original Burrito: modules, configuration keys, environment variables and plugins all use the
burrito_ng/BurritoNgnames, and versioning restarts at 1.0.0.
Cross-Platform Elixir Deployments
What Is It?
Background
Burrito NG is our answer to the problem of distributing Elixir CLI applications across varied environments, where we cannot guarantee that the Erlang runtime is installed, and where we lack the permissions to install it ourselves. In particular, we have CLI tooling that must be deployed on-premise, by consultants, into customer environments that may be running MacOS, Linux, or Windows.
Furthermore, these tools depend on NIFs that we need to cross-compile for any of the environments that we support, from one common build server, running in our CI environment.
We were heavily inspired by Bakeware, which lays a lot of the ground work for our approach. Ultimately we implemented and expanded upon many of Bakeware's ideas, originally in Zig, and today in Rust.
Feature Overview
- Builds a self-extracting archive for a Mix project, targeting Windows, MacOS, and Linux, containing:
- Your compiled BEAM code
- The required ERTS for your project
- Compilation artifacts for any elixir-make based NIFs used by the project
- Provides a "plugin" interface for injecting Rust code into your application's boot sequence
- We use this to perform automatic updates and licensing checks (see
lib/versions/release_file.exfor details)
- We use this to perform automatic updates and licensing checks (see
- Automatically uninstalls old versions of the payload if a new version is run.
Supported Versions:
Burrito NG is tested with Elixir 1.20 on Erlang/OTP 27, 28 and 29.
The upstream Burrito project provides pre-compiled Erlang/OTP distributions starting from OTP-25.3 onwards for MacOS, Linux, and Windows targets. A pre-compiled distribution must exist for the exact OTP version installed on your build machine.
If you require an older version, please refer to the section about (providing custom Erlang/OTP builds)[#using-custom-erts-builds].
Technical Component Overview
Burrito NG is composed of a few different components:
- Mix Release Module - A module that is executed as a Mix release step. This module takes care of packing up the files, downloading and copying in different Erlang VM Runtime files, and building the Rust Wrapper.
- Payload - The release directory, containing all the compiled BEAM code for your release and the ERTS for the target platform, packed into a tar archive (with the
tarcrate) and xz compressed (with the pure Rustlzma-rust2crate). This is done by the wrapper's Cargo build script (build.rs), and the result is embedded directly into the wrapper program. - Rust Wrapper - This is portable cross-platform Rust code that wraps around the payload generated during the Mix release process. Erlang is launched in Embedded Mode (for more details see System Principles) as a child process of the wrapper.
Burrito NG Produced Binary
┌────────────────────────────────┐
│ │
│ Rust Wrapper Binary │ <---- Compiled from `src/main.rs`
│ │
├────────────────────────────────┤
│ Payload Archive │
│ ┌────────────────────────────┐ │
│ │ │ │
│ │ ERTS Native Binaries │ <------ If cross-compiling, this is downloaded from a build server
│ │ │ │
│ └────────────────────────────┘ │
│ │ <---- This bottom payload portion is generated by `build.rs`
│ ┌────────────────────────────┐ │
│ │ │ │
│ │ Application BEAM Code │ │
│ │ │ │
│ └────────────────────────────┘ │
│ │
└────────────────────────────────┘End To End Overview
- You build a Burrito NG wrapped binary of your application and send it to an end-user
- The end-user launches your binary like any other native application on their system
- In the background (first-run only) the payload is extracted into a well defined location on the system. (AppData, Application Support, etc.)
- The wrapper executes the Erlang runtime in the background, and transparently launches your application within the same process
- Subsequent runs of the same version of that application will use the previously extracted payload
Quick Start
Disclaimer
Burrito NG was built with our specific use case in mind, and while we've found success with deploying applications packaged using Burrito NG to a number of production environments, the approach we're taking is still experimental.
That being said, we're excited by our early use of the tooling, and are eager to accept community contributions that improve the reliability of Burrito NG, or that add support for additional platforms.
Preparation and Requirements
The matrix below outlines which build targets are currently supported by each host.
| Target | Host | Host | Host | Host |
|---|---|---|---|---|
| Windows x64 | Linux | MacOS (x86_64) | MacOS (Apple Silicon) | |
| Windows x64 | ❌ | ✅ | ✅ | ✅ |
| Linux | ❌ | ✅ | ✅ | ✅ |
| MacOS (x86_64) | ❌ | ⚠️ (requires osxcross) | ✅ | ✅ |
| MacOS (Apple Silicon) | ❌ | ⚠️ (requires osxcross) | ✅ | ✅ |
We support targeting Windows (x8664) from MacOS and Linux, we _do not officially support building ON Windows, it's recommended you use WSL if your development machine is Windows.
You must have the following installed and in your PATH:
- Rust (1.85 or newer) --
cargo, installed with rustup - The Rust standard library for each of your targets:
rustup target add <triple>- Linux:
x86_64-unknown-linux-musl,aarch64-unknown-linux-musl - MacOS:
x86_64-apple-darwin,aarch64-apple-darwin - Windows:
x86_64-pc-windows-gnu
- Linux:
- 7z --
7z(For Windows Targets)
The wrapper's Rust dependencies are fetched from crates.io by Cargo the first time a wrapper is built, and are cached afterwards.
Depending on your targets you will also need some platform toolchains:
- Linux targets need nothing extra to build the wrapper, it is linked with the
rust-lldlinker that ships with Rust. - Windows targets need the MinGW-w64 linker (
x86_64-w64-mingw32-gcc). Debian/Ubuntu:apt install mingw-w64, MacOS:brew install mingw-w64. - MacOS targets build out of the box on MacOS (with the Xcode command line tools). From Linux you need a MacOS SDK and linker such as osxcross, configured through Cargo's standard linker variable, e.g.
CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER=aarch64-apple-darwin23-clang.
You can always override the linker Burrito NG picks for a target with Cargo's CARGO_TARGET_<TRIPLE>_LINKER environment variable.
NIFs: If your project uses elixir_make based NIFs, Burrito NG re-compiles them for every cross-build target (Linux targets always count as cross-builds, since our pre-compiled ERTS is linked against musl). This needs a C cross compiler for each target, which Burrito NG looks for in your PATH:
| Target | Default C compiler | Where to get it |
|---|---|---|
| Linux | <cpu>-unknown-linux-musl-gcc (or <cpu>-linux-musl-gcc) | Linux: cross-tools/musl-cross, MacOS: brew install messense/macos-cross-toolchains/<cpu>-unknown-linux-musl |
| Windows | <cpu>-w64-mingw32-gcc | apt install mingw-w64 / brew install mingw-w64 |
| MacOS | clang -target <cpu>-apple-macos11 | Xcode command line tools (MacOS hosts only) |
You can point Burrito NG at a different compiler with the nif_cc / nif_cxx / nif_ar / nif_ranlib qualifiers.
Mix Project Setup
Add
burrito_ngto your list of dependencies:defp deps do [{:burrito_ng, "~> 1.0"}] endIf you are migrating from the original
burrito, note that Burrito NG is not a drop-in replacement:Burrito.*modules are nowBurritoNg.*(e.g.&BurritoNg.wrap/1,BurritoNg.Util.Args).- The release config key is
burrito_ng:instead ofburrito:. - Environment variables use the
BURRITO_NG_prefix (e.g.BURRITO_NG_TARGET,__BURRITO_NG_BIN_PATH). - Plugins are Rust files with a
burrito_ng_plugin_entryfunction, instead of Zig files. - Apps are installed under
.burrito_nginstead of.burrito, and binaries are written toburrito_ng_out/.
Create a
releasesfunction in yourmix.exs, add and configure the following for your project:def releases do [ example_cli_app: [ steps: [:assemble, &BurritoNg.wrap/1], burrito_ng: [ targets: [ macos: [os: :darwin, cpu: :x86_64], macos_silicon: [os: :darwin, cpu: :aarch64], linux: [os: :linux, cpu: :x86_64], windows: [os: :windows, cpu: :x86_64] ] ] ] ] end
(See the Mix Release Config Options for additional options)
- Add the releases function into your project function:
def project do
[
# ... other project configuration
releases: releases()
]
endTo build a release for all the targets defined in your
mix.exsfile:MIX_ENV=prod mix releasePer-target binaries are now available in the
burrito_ng_out/folder.You can also build a single target by setting the
BURRITO_NG_TARGETenvironment variable to the alias for that target (e.g. SettingBURRITO_NG_TARGET=macosbuilds only themacostarget defined above.)
NOTE: In order to speed up iteration times during development, if the Mix environment is not set to prod, the binary will always extract its payload, even if that version of the application has already been unpacked on the target machine.
Mix Release Config Options
targets- A list of atoms, the targets you want to build for (:darwin,:win64,:linux,:linux_musl) whenever you run amix releasecommand -- if not defined, defaults to native host platform only.debug- Boolean, will produce a debug build if set to true. (Default:false)no_clean- Boolean, will not clean up after building if set to true. (Default:false)plugin- String, a path to a Rust source file (.rs) that contains a functionpub fn burrito_ng_plugin_entry(install_dir: &str, program_manifest_json: &str), which will be called before unpacking the payload at runtime. The file is compiled into the wrapper and may only use the Rust standard library. See the example application for details.
Build-Time Environment Variables
BURRITO_NG_TARGET- Override the list of targets provided in your release configuration. (ex:BURRITO_NG_TARGET=win64,BURRITO_NG_TARGET=linux,darwin)
Application Entry Point
For Burrito NG to work properly you must define a :mod in your project's Mix config:
def application do
[
mod: {MyEntryModule, []}
]
endThis module must implement the callbacks defined by the Application module, as stated in the Mix documentation:
defmodule MyEntryModule do
def start(_, _) do
# Returning `{:ok, pid}` will prevent the application from halting.
# Use System.halt(exit_code) to terminate the VM when required
end
endIf you wish you retrieve the argv passed to your program by Burrito NG use this snippet:
args = BurritoNg.Util.Args.argv() # this returns a list of stringsMaintenance Commands
Binaries built by Burrito NG include a built-in set of commands for performing maintenance operations against the included application:
./my-binary maintenance uninstall- Will prompt to uninstall the unpacked payload on the host machine../my-binary maintenance directory- Will print the path to the installation directory for the unpacked payload on the host machine../my-binary maintenance meta- Will print the metadata for binary.
Advanced Build Configuration
Build Steps and Phases
Burrito NG runs the mix release task in three "Phases". Each of these phases contains a number of "Steps", and a context struct containing the current state of the build, which is passed between each step.
The three phases of the Burrito NG build pipeline are:
Fetch- This phase is responsible for downloading or locally locating any replacement ERTS builds for cross-build targets.Patch- This phase copies replacement ERTS files, and re-compiles NIFs (if any). This phase is also where any custom files should be copied into the build directory before being archived.Build- This is the final phase in the build flow, it produces the final wrapper binary with the payload embedded inside.
You can add your own steps before and after phases execute. Your custom steps will also receive the build context struct, and can return a modified one to customize a build to your liking.
An example of adding a step before the fetch phase, and after the build phase:
# ... mix.exs file
def releases do
[
my_app: [
steps: [:assemble, &BurritoNg.wrap/1],
burrito_ng: [
# ... other Burrito NG configuration
extra_steps: [
fetch: [pre: [MyCustomStepModule, AnotherCustomStepModule]],
build: [post: [CustomStepAgain, YetAnotherCustomStepModule]]
]
]
]
]
end You can override default steps or phases by setting the phases option.
If your build phase's requirements are different to Burrito NG's, specify your own assemble step that calls BurritoNg.Builder.build(release).
# ... mix.exs file
def releases do
[
my_app: [
steps: [:assemble, &MyProject.wrap/1],
burrito_ng: [
# ... other Burrito NG configuration
phases: [
build: [MyProject.CustomBuildStep1, MyProject.CustomBuildStep2]
]
]
]
]
end defmodule MyProject
def wrap(%Mix.Release{} = release) do
pre_check(release)
BurritoNg.Builder.build(release)
end
defp pre_check(release) do
# checks specific to your build.
end
endBuild Targets and Qualifiers
A Burrito NG build target is a keyword list that contains an operating system, a CPU architecture, and extra build options (called Qualifiers).
Here's a definition for a build target configured for Linux x86-64 that adds extra CFLAGS to the NIF recompile step:
targets: [
linux: [os: :linux, cpu: :x86_64, nif_cflags: "-DSOME_DEFINE"]
]Build qualifiers are a simple way to pass specific flags into the Burrito NG build pipeline. Here is a list of the supported qualifiers:
custom_erts-binary()orURI.t()that points to a custom tar.gz. See Using custom ERTS builds for more info.nif_cflags-binary()String that is appended to the end ofCFLAGSwhen recompiling NIFs for another target.nif_cxxflags-binary()String that is appended to the end ofCXXFLAGSwhen recompiling NIFs for another target.nif_env-list(tuple())List of 2-tuples (strings) that define environment variables when recompiling NIFs for another target.nif_make_args-binary()String that is appended to themakecall when Elixir make is invoked for recompiling NIFs.nif_cc/nif_cxx-binary()The C / C++ compiler command used when recompiling NIFs for another target (e.g."x86_64-linux-musl-gcc"). See Preparation and Requirements for the defaults.nif_ar/nif_ranlib-binary()Thear/ranlibcommands used when recompiling NIFs for another target.skip_nifs-boolean()Boolean value, defaults tofalse, if set totrueNIFs will NOT be recompiled. Use this if you want to copy in NIFs that you recompiled yourself in combination with extra/custom build steps.
Tip: You can use these qualifiers as a way to pass per-target information into your custom build steps.
Using Custom ERTS Builds
The upstream Burrito project provides precompiled builds of Erlang for the following platforms:
[os: :darwin, cpu: :x86_64],
[os: :darwin, cpu: :aarch64],
[os: :linux, cpu: :x86_64],
[os: :linux, cpu: :aarch64],
[os: :windows, cpu: :x86_64]If you require a custom build of ERTS, you're able to override the precompiled binaries on a per target basis by setting custom_erts to the path of your ERTS build:
targets: [
linux_arm: [
os: :linux,
cpu: :aarch64,
custom_erts: "/path/to/my_custom_erts.tar.gz"
]
]The custom_erts value should be a path to a local .tar.gz of a release from the Erlang source tree. The structure inside the archive should mirror:
. (TAR Root)
└─ otp-A.B.C-OS-ARCH
├─ erts-X.Y.Z/
├─ releases/
├─ lib/
├─ misc/
├─ usr/
└─ InstallYou can easily build an archive like this by doing the following commands inside the official Erlang source code:
# configure and build Erlang as you require...
# ...
export RELEASE_ROOT=$(pwd)/release/otp-A.B.C-OS-ARCH
make release
cd release
tar czf my_custom_erts.tar.gz otp-A.B.C-OS-ARCH
Known Limitations and Issues
Phx Tips
In order to run a Phoenix app with Burrito NG, you may need to edit your runtime.exs file to ensure it will always start up the server:
# This is the default line in runtime.exs, you can remove the if statement wrapping this config line, or check for other conditions:
if System.get_env("PHX_SERVER") do
config :phx_app, PhxAppWeb.Endpoint, server: true
endIf you do not want to edit runtime.exs just ensure the environment variable PHX_SERVER is set to 1 when you launch your Burrito NG wrapped binary. Otherwise it will simply exit without starting your server application.
Additionally, you should take care to ensure you compile your assets BEFORE you wrap your application with Burrito NG.
Runtime Requirements
Minimizing the runtime dependencies of the package binaries is an explicit design goal, and the requirements for each platform are as follows:
Windows
- MSVC Runtime for the Erlang version you are shipping
- Windows 10 Build 1511 or later (for ANSI color support)
Linux
- No runtime dependencies.
MacOS
- No runtime dependencies. however a security exemption must be set in MacOS Gatekeeper unless the binary undergoes code-signing.
Contributing
Welcome!
We are happy to review and accept pull requests to improve Burrito NG at MudrockSoftware/burrito-ng, and ask that you follow the established code formatting present in the repo!
Everything in this repo is licensed under The MIT License, see LICENSE for the full license text.
Burrito NG is based on the original Burrito project. Many thanks to its authors and contributors.