Introduction
RabbitMQ streams is built on top of osiris. osiris
models an append-only persistent log and I though it would be interesting to see if
it would be possible to use it outside of RabbitMQ.
The problem
osiris
is not available through hex.pm so we include it using the github directive:
# mix.exs
defp deps do
{:osiris, github: "rabbitmq/osiris"}
end
But compiling results in an error
mix deps.get
mix deps.compile
===> Compilation failed: there is no code in this directory (/$HOME/dev/deps_test/_build/dev/lib/osiris), it is unreadable or for some other reason is not a recognizable application structure.
** (Mix) Could not compile dependency :osiris, "/$HOME/.asdf/installs/elixir/1.13.1-otp-24/.mix/rebar3 bare compile --paths /$HOME/deps_test/_build/dev/lib/*/ebin" command failed. Err
This is likely due to mix
interpreting osiris
as rebar3 project due to the rebar.config
in the root of the project. By specifying the :manger
option to use :make
the automatic project type inference is bypassed:
# mix.exs
defp deps do
{:osiris, github: "rabbitmq/osiris", manager: :make}
end
So now it compiles but if we try run the application the following happens:
mix
** (Mix) Could not start application gen_batch_server: could not find application file: gen_batch_server.app
When osiris
is compiled the dependencies gen_batch_server
and seshat
are fetched. However the dependencies are not added to the deps
directory but is found in deps/osiris/deps
. We can fix this by adding the dependencies to the project, since they’re both available through hex
:
defp deps do
[
{:osiris, github: "rabbitmq/osiris", manager: :make},
# Osiris deps,
{:gen_batch_server, "~> 0.8.8", override: true},
{:seshat, "~> 0.3.2", override: true},
]
end
Another approach is to include the dependencies via the :path
directive by resolving them ourself:
def project do
[
app: :deps_test,
version: "0.1.0",
elixir: "~> 1.13",
start_permanent: Mix.env() == :prod,
deps: deps(),
aliases: aliases()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
deps = [
{:osiris, github: "rabbitmq/osiris", manager: :make}
]
nested_deps = resolve_deps(:osiris, [:gen_batch_server, :seshat], Mix.Project.deps_path())
deps ++ nested_deps
end
defp mk_deps(_) do
path = Path.join([Mix.Project.deps_path(), "osiris"])
File.cd!(path, fn ->
System.cmd("make", ["fetch-deps"])
end)
end
defp resolve_deps(name, deps, deps_path) do
base_path = Path.join([deps_path, to_string(name)])
for dep <- deps do
build_dep(dep, base_path)
end
end
defp build_dep(name, path) do
{
name,
path: Path.join([path, "deps", to_string(name)]),
}
end
defp aliases do
[
deps_all: [
"deps.get",
&mk_deps/1
]
]
end
mix deps.get
fetches osiris
but its dependencies are not fetched until mix deps.compile
is called. (since deps.compile
will run make
in deps/osiris
).
We define the alias deps_all
which calls mix deps.get
and then calls make fetch-deps
from osiris
making them available on the paths previously defined using resolve_deps/3
.
Another slightly more automatic but equally “hacky” approach would be to a check in deps/0
if the osiris
directory exists so that we can call make list-deps
and create path dependencies for all dependencies listed:
defmodule DepsTest.MixProject do
use Mix.Project
def project do
[
app: :deps_test,
version: "0.1.0",
elixir: "~> 1.13",
start_permanent: Mix.env() == :prod,
deps: deps(),
aliases: aliases()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
deps = [
{:osiris, github: "rabbitmq/osiris", manager: :make}
]
nested_deps = resolve_deps(:osiris, Mix.Project.deps_path())
deps ++ nested_deps
end
defp mk_deps(_) do
path = Path.join([Mix.Project.deps_path(), "osiris"])
File.cd!(path, fn ->
System.cmd("make", ["fetch-deps"])
end)
end
defp resolve_deps(name, deps_path) do
base_path = Path.join([deps_path, to_string(name)])
if File.exists?(base_path) do
File.cd!(base_path, fn ->
case System.cmd("make", ["list-deps"]) do
{deps, 0} ->
deps
|> String.split("\n", trim: true)
|> Enum.map(&build_dep/1)
{out, code} ->
raise "`make list-deps` error: #{inspect(out)} code: #{code}"
end
end)
else
[]
end
end
defp build_dep(path) do
name = Path.basename(path)
{
String.to_atom(name),
path: path,
}
end
defp aliases do
[
deps_all: [
"deps.get",
&mk_deps/1
]
]
end
end
NOTE! We are still required to call mix deps_all
(or mix deps.get
) before mix deps.compile
.
Conclusion
I think it’s a neat feature in mix
that multiple build tools can be used and even though the proposed solutions are “hacks” it does get the job done.