Using Mix.install
Despite being a compiled language, Elixir has provided support for running code in a single file as though it were a scripting language since its release. When processing a file with the .exs
extension Elixir will compile the file in-memory and run the compiled code. In this post, we will cover an experimental feature that is expanding upon the language's scripting abilities.
A Basic Elixir Script
To introduce scripting with Elixir, we can create a file, my_script.exs
, to print something "Hello, World"-like.
# my_script.exs
IO.puts("Hello, from Elixir")
IO.puts("...a scripting language?")
We can run our .exs
file by passing its pathname to the elixir
command.
› elixir my_script.exs
Hello, from Elixir
...a scripting language?
This article covers more on how this works.
Bringing in Dependencies
To further extend Elxir's ability to be used in this scripting context, a new experimental feature has been added in Elixir's 1.12 release, Mix.install
. With Mix.install
, you can list third-party packages to use in your script like you would in a mix.exs
file. If you are familiar with the Ruby ecosystem, this is similar to the inline functionality provided by Bundler.
A Basic Mix.install
To start with something simple, let's steal an example from the documentation: JSON-encoding a map with the Jason package.
# mix_install_test.exs
Mix.install([:jason])
IO.puts(Jason.encode!(%{hello: :world}))
When running, we see the following output:
Resolving Hex dependencies...
Dependency resolution completed:
New:
jason 1.2.2
* Getting jason (Hex package)
==> jason
Compiling 8 files (.ex)
Generated jason app
{"hello":"world"}
We first resolve, fetch, and compile our dependencies; this should look familiar to you if you've used mix deps.get
before. Once Mix.install
is complete and we have our dependencies, we run our IO.puts
and output the encoded JSON.
Caching
Our dependencies are cached as a part of our first run; this means subsequent runs will not need to include any dependency management!
› elixir mix_install_test.exs
# no resolving dependencies!
{"hello":"world"}
To find out where the dependencies are cached on your system, you can pass the verbose
option to Mix.install
.
Mix.install(
[:jason],
verbose: true
)
Now, when we run our script, it will output the path to the dependency cache.
› elixir mix_install_test.exs
using /Users/me/Library/Caches/mix/installs/elixir-1.12.0-erts-12.0/11989020f314102159a0c9ca882052fc
Caching is based on a combination of Elixir and OTP versions, as well as the dependencies you have listed. Changing any of these (or setting the force
flag) will result in a cache miss and require re-fetching and compiling packages.
Mix Options
Mix.install
works by dynamically building a Mix project for you when the script runs. As a result, the dependency list passed to Mix.install
is the same as your deps
list in a Mix project's mix.exs
. By working with an in-memory Mix project, you can take full advantage of Mix dependency management, including specifying package versions and all other options provided by Mix.
Another Example
One possible use case I imagined for using Mix.install
was to explore new APIs. As an example, I wanted to fetch the current price of Bitcoin from the Coinbase API.
When creating the script, I decided to use Mojito for HTTP requests. Unfortunately, I ran into an issue using OTP 24 with Mint, the underlying packages Mojito is built on. This issue was resolved on the main
branch, but had not yet been released. By leveraging the git options provided by Mix, I was able to point at the main
branch to use the latest code. Since I was actually using Mojito, I was also able to leverage the override
option to tell Mix to use my overridden version of the dependency. Because Mix.Install
provides the full power of Mix dependency management, I was able to easily work around the temporary issues and create a script that solved my problem.
Mix.install(
[
:jason,
:mojito,
{
:mint,
git: "https://github.com/elixir-mint/mint",
branch: "main",
override: true
}
]
)
{:ok, %{body: body}} =
Mojito.request(
method: :get,
url: "https://api.coindesk.com/v1/bpi/currentprice.json"
)
bit_coin_rate =
body
|> Jason.decode!()
|> get_in(["bpi", "USD", "rate"])
IO.puts("The current rate for Bitcoin is #{bit_coin_rate}")
We can now run this program as though it were a Ruby or Python script to track our Bitcoin investment 💎 🙌 🚀!
› elixir bitcoin_price.exs
The current rate for Bitcoin is 55,277.6167
› elixir bitcoin_price.exs
The current rate for Bitcoin is 55,168.1100
Future
Because we still need Elixir installed on our system to run the script, this doesn't provide us with the portability of something like Go's ability to build executable binaries. For that, you may want to explore Bakeware. For something between a single file script and Bakeware
, you may also want to investigate escript. With escript
, you can build your mix
project into an executable. It does, however, require Erlang to be installed on the system running the program.
While you may need to reach for more robust options, Mix.install
provides another tool in the Elixir toolbelt. With the addition of a single function, Elixir has increased its abilities to write small scripts and be used in more ways.