Burrito 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 using Zig.
lib/versions/release_file.ex
for details)We provide pre-compiled Erlang/OTP distributions starting from OTP-25.3
onwards for MacOS, Linux, and Windows targets.
If you require an older version, please refer to the section about providing custom Erlang/OTP builds.
Burrito is composed of a few different components:
execve()
(on Windows we use a child process). Burrito Produced Binary
┌────────────────────────────────┐
│ │
│ Zig Wrapper Binary │ <---- Compiled from `wrapper.zig`
│ │
├────────────────────────────────┤
│ Payload Archive │
│ ┌────────────────────────────┐ │
│ │ │ │
│ │ ERTS Native Binaries │ <------ If cross-compiling, this is downloaded from a build server
│ │ │ │
│ └────────────────────────────┘ │
│ │ <---- This bottom payload portion is generated by `archiver.zig`
│ ┌────────────────────────────┐ │
│ │ │ │
│ │ Application BEAM Code │ │
│ │ │ │
│ └────────────────────────────┘ │
│ │
└────────────────────────────────┘
Burrito was built with our specific use case in mind, and while we've found success with deploying applications packaged using Burrito 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, or that add support for additional platforms.
NOTE: Due to current limitations of Zig, some platforms are less suited as build machines than others: we've found the most success building from Linux and MacOS. 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) | ❌ | ✅ | ✅ | ✅ |
MacOS (Apple Silicon) | ❌ | ✅ | ✅ | ✅ |
We support targeting Windows (x86_64) 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:
zig
xz
7z
(For Windows Targets)Add burrito
to your list of dependencies:
defp deps do
[{:burrito, "~> 1.0"}]
end
Create a releases
function in your mix.exs
, add and configure the following for your project:
def releases do
[
example_cli_app: [
steps: [:assemble, &Burrito.wrap/1],
burrito: [
targets: [
macos: [os: :darwin, cpu: :x86_64],
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()
]
end
To build a release for all the targets defined in your mix.exs
file: MIX_ENV=prod mix release
You can also build a single target by setting the BURRITO_TARGET
environment variable to the alias for that target (e.g. Setting BURRITO_TARGET=macos
builds only the macos
target 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.
targets
- A list of atoms, the targets you want to build for (:darwin
, :win64
, :linux
, :linux_musl
) whenever you run a mix release
command -- 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 Zig file that contains a function burrito_plugin_entry()
which will be called before unpacking the payload at runtime. See the example application for details.BURRITO_TARGET
- Override the list of targets provided in your release configuration. (ex: BURRITO_TARGET=win64
, BURRITO_TARGET=linux,darwin
)For Burrito to work properly you must define a :mod
in your project's Mix config:
def application do
[
mod: {MyEntryModule, []}
]
end
This 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
end
If you wish you retrieve the argv passed to your program by Burrito use this snippet:
args = Burrito.Util.Args.argv() # this returns a list of strings
Binaries built by Burrito 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.
Burrito 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 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, &Burrito.wrap/1],
burrito: [
# ... other Burrito 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's, specify your own assemble
step that calls Burrito.Builder.build(release)
.
# ... mix.exs file
def releases do
[
my_app: [
steps: [:assemble, &MyProject.wrap/1],
burrito: [
# ... other Burrito configuration
phases: [
build: [MyProject.CustomBuildStep1, MyProject.CustomBuildStep2]
]
]
]
]
end
defmodule MyProject
def wrap(%Mix.Release{} = release) do
pre_check(release)
Burrito.Builder.build(release)
end
defp pre_check(release) do
# checks specific to your build.
end
end
A Burrito 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 build pipeline. Here is a list of the supported qualifiers:
custom_erts
- binary()
or URI.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 of CFLAGS
when recompiling NIFs for another target.nif_cxxflags
- binary()
String that is appended to the end of CXXFLAGS
when 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 the make
call when Elixir make is invoked for recompiling NIFs.skip_nifs
- boolean()
Boolean value, defaults to false
, if set to true
NIFs 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.
The 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/
└─ Install
You can easily build an archive like this by doing the following commands inside the (official Erlang source code)[https://github.com/erlang/otp]:
# 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
In order to run a Phoenix app with Burrito, 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
end
If you do not want to edit runtime.exs
just ensure the environment variable PHX_SERVER
is set to 1
when you launch your burrito 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.
Minimizing the runtime dependencies of the package binaries is an explicit design goal, and the requirements for each platform are as follows:
We are happy to review and accept pull requests to improve Burrito, 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.