Bakeware is a new fantastic tool, built (by Frank Hunleth, Jon Carstens and Connor Rigby) over a weekend for the SpawnFest 2020, which compiles an Elixir, a Scenic or a Phoenix application into single executable binary (yes, like go-lang!). It can be extremely useful to distribute our apps, especially when they are command-line tools or Scenic applications.
For most of the Phoenix app deployments, this tool maybe is not crucial, but I think that sometime it can be really useful to have a Phoenix/Phoenix LiveView app (along with all the needed assets!) in a single, and easily sharable, binary.
Let’s see how to make it work with a Phoenix LiveView application. What we are going to see applies to CLIs and Scenic apps as well.
Bakeware library and Counter LiveView example
At the moment there isn’t an official library on hex.pm, so let’s start by downloading the Bakeware’s code from its GitHub repo, spawnfest/bakeware.
$ git clone https://github.com/spawnfest/bakeware.git
Cloning into 'bakeware'...
...
$ ls bakeware/bakeware
Makefile README.md assets lib mix.exs mix.lock src test
In the bakeware/examples folder, you can also find other useful examples. The library code we need is inside the bakeware/bakeware directory.
To see Bakeware in action, let’s create a new Phoenix project called counter, with LiveView support and without Ecto.
$ mix phx.new counter --live --no-ecto
...
$ cd counter
In the counter
Phoenix project’s directory, we create a lib/counter_web/live/counter_live.ex
file where we define a CounterWeb.CounterLive
live view. You can can simply copy/paste the module’s code below.
#lib/counter_web/live/counter_live.ex
defmodule CounterWeb.CounterLive do
use CounterWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, counter: 0)}
end
@impl true
def render(assigns) do
~L"""
<h1>Counter: <%= @counter %></h1>
<button phx-click="dec">-</button>
<button phx-click="inc">+</button>
"""
end
@impl true
def handle_event("inc", _, socket) do
{:noreply, update(socket, :counter, & &1 + 1)}
end
@impl true
def handle_event("dec", _, socket) do
{:noreply, update(socket, :counter, & &1 - 1)}
end
end
and update the "/"
live route in lib/counter_web/router.ex
.
#lib/counter_web/router.ex
defmodule CounterWeb.Router do
use CounterWeb, :router
...
scope "/", CounterWeb do
pipe_through :browser
live "/", CounterLive, :index
end
end
Ok, we now have a simple LiveView counter application that we can compile with Bakeware.
To use Bakeware we, just need to change the mix.exs
file of our counter
Phoenix project.
#mix.exs
defmodule Counter.MixProject do
def project do
[
app: :counter,
...
releases: [
baked_counter: [
steps: [:assemble, &Bakeware.assemble/1],
strip_beams: Mix.env() == :prod,
overwrite: true
]
]
]
end
defp aliases do
[
assets: ["cmd npm run deploy --prefix assets"],
release: ["assets", "phx.digest", "release"],
setup: ["deps.get", "cmd npm install --prefix assets"]
]
end
defp deps do
[
...
{:bakeware, path: "../bakeware/bakeware", runtime: false}
]
end
end
We first add the :bakeware
dependency, pointing the :path
to the local bakerware subfolder and setting the :runtime
option to false
.
We then add the :release
and :assets
aliases, which shorten the list of command we need to type to build a release. For example, the :release
alias runs mix assets
, mix phx.digest
and mix release
.
Most importantly, we add a :releases
option in the keyword list returned by project/0
. The only release we build is called baked_counter
, and we set [:assemble, &Bakeware.assemble/1]
as a list of :steps
to execute when assembling the release. We set two other options: overwrite: true
(if there is an existing release version, overwrite it) and :strip_beams
which is true
only when the environment is prod
(controls if BEAM files should have their debug information, documentation chunks, and other non-essential metadata removed).
Remember to add the server: true
option to the CounterWeb.Endpoint
production config, in config/prod.exs
#config/prod.exs
config :counter, CounterWeb.Endpoint,
...
server: true,
check_origin: false
When Setting server: true
the web server is started when the endpoint supervision tree starts. We can also add check_origin: false
, which disables the check of the origin header.
We are ready to build the release. Before running the mix
tasks, we need to SECRET_KEY_BASE
env variables.
$ mix phx.gen.secret
y0RanKUeQ641fq8Mzh/sa0WwuVoBxwqVrgsXM+aGPrtpYRZwwuyoRRZpomw8ALqJ
$ export SECRET_KEY_BASE="y0RanKUeQ641fq8Mzh/sa0WwuVoBxwqVrgsXM+aGPrtpYRZwwuyoRRZpomw8ALqJ"
And then we run the setup
and release
mix tasks, setting the MIX_ENV
env variable to prod
.
$ MIX_ENV=prod mix setup
...
$ MIX_ENV=prod mix release
...
* assembling bakeware baked_counter
Bakeware successfully assembled executable \
at _build/prod/rel/bakeware/baked_counter
The baked_counter
executable binary is ready, we find it in the _build/prod/rel/bakeware
folder.
When we run it we see that our server starts correctly, serving the web app assets (in this case just the javascript, css and Phoenix Framework logo).
$ ./_build/prod/rel/bakeware/baked_counter
bakeware: starting '/Users/alvise/Documents/poeticoding/bakeware/code/counter/./_build/prod/rel/bakeware/baked_counter' (cachedir=/Users/alvise//Library/Caches/Bakeware)...
bakeware: Cache invalid. Extracting...
bakeware: Running /Users/alvise//Library/Caches/Bakeware/6ea5f818ebb68300b8d953d54e082779006802152906cd90dc43caeaefc9a19f/start...
16:49:25.455 [info] Running CounterWeb.Endpoint with cowboy 2.8.0 at :::4000 (http)
16:49:25.456 [info] Access CounterWeb.Endpoint at http://example.com
How does Bakeware work?
The executable binary, built by Bakeware, has the whole release compressed in it. When we run it, it extracts everything in a cache folder, and starts the application.
To optimize the start-time, Bakeware maintains a cache of extracted binaries and assets. In this way, the second time we run it, it’s faster since it doesn’t need to uncompress anything.
The cache directory location is system-specific:
- on macOS is
"~/Library/Caches/Bakeware"
- on Linux and other Unixes is
"~/.cache/bakeware"
- on Windows is
"C:/Users/<USER>/AppData/Local/Bakeware/cache"
In this case, I’m on a mac. If we go in the ~/Library/Caches/Bakeware/6ea5f...
folder, we find the extracted release.
And under lib/counter-1.0/priv/static
we the app static assets.
Share the app with another computer
Let’s try the baked_counter
on another mac. If you built your app on Linux, it should work on other similar Linux environments. Windows support is coming (take a look at the end of the article).
I’ve compiled it with my MacBook Pro with Catalina (10.15.7), so it would be nice to see if it works on a slightly older macOS version, like Mojave (10.14). I don’t have another Mac, so I use a remote macmini with macOS 10.14, hosted by MacinCloud, with a pay-as-you-go plan.
And it works!! Is it compatible across many OS versions? Well… it depends by different things. When we build a release this is supposed to run on a target machine with the same operating system with a similar environment, like C runtime and other libraries referenced by the Erlang runtime and any NIFs and ports in the application.
Wrap Up
This is a project started just a month ago, and the team has planned to work on many other things, like for example:
- <<The Windows support seems to be not too far off!>>
- <<Cross-compilation is something to consider as a future feature>>
Do you think Bakeware is going to be helpful for you? Maybe for a CLI written in Elixir? Or a Scenic, or Phoenix app with LiveView? Let me know what you think!