MBU - Mix Build Utilities

tl;dr

MBU is a collection of functions and a few macros that I wrote to help in using Mix as a build tool for e.g. frontend building. It supports task dependencies, and watching paths (either with watchers built in to different tools or custom watchers using file_system). You can find it on Hex, documentation on Hexdocs, and code on GitLab.

Motivation

This blog post explains my motivations best (has old code samples but the motivations are the same). The short version is that I was tired of learning different frontend build systems that all seemed to be magical, fragile, or difficult to maintain. I thought about Make and NPM scripts but ended up with my own solution that would serve my own needs: use Mix as a build tool to call the usual Node tools, so that I can write my build scripts in Elixir and know how they work.

If I succeeded in creating anything useful is left up to the reader. :sweat_smile:

How it works

Let’s suppose the following config:

config :mbu,
  auto_paths: true, # Autogenerate output path for tasks
  create_out_paths: true, # Create output path when task runs
  tmp_path: Path.expand(".tmp") # Path under which autogenerated output paths are, i.e. where task output is stored

Then we could have the following build task:

defmodule Mix.Task.Css.Compile do
  use MBU.BuildTask

  @deps ["css.autoprefix", "assets.copy"]

  def in_path(), do: Path.join("assets", "css") |> Path.expand()
  def in_file(), do: Path.join(in_path(), "frontend.scss")

  def args() do
    [
      "--source-map-embed",
      "--output",
      out_path(),
      in_file()
    ]
  end

  task _ do
    exec("node_modules/.bin/node-sass", args()) |> listen()

    out_file = Path.basename(in_file(), "scss") <> "css"
    print_size(Path.join(out_path(), out_file))
  end
end

What this task does is:

  1. Runs the dependencies listed in @deps in parallel before the task is run.
  2. Creates an output directory for the build artifacts based on the task name and :tmp_path, in this case it would be something like /path/to/proj/.tmp/css.compile. This path is returned by out_path/0.
  3. Executes the node-sass command with the given arguments, waits for it to complete and prints the output.
  4. Prints the size of the compiled file using print_size/1.

Now if we wanted to watch the assets/css directory for changes, we could create another task and in that task’s task block, we can do:

watch("FrontendCompileCSS", Mix.Task.Css.Compile.in_path(), Mix.Task.Css.Compile)
|> listen(watch: true)

What this does is it listens to the path given in the second argument for changes, and then runs the task given in the third argument (the first argument is for logging purposes). The last argument could also be an anonymous function to call. The watch: true option makes the listen/2 function also listen for an enter key from the user, which will stop the watching.

This was just a small example, for more realistic usage, see my website’s build tasks at https://gitlab.com/code-stats/code-stats/tree/master/lib/mix/tasks (probably a bit overengineered at the moment as it will have more complex targets in the future).

Pros & cons

I am obviously biased and blind to my own creation but here’s what I think of it:

  • Pros:
    • Minimal features & magic
    • Get to write Elixir, no external deps
    • Easier to fix/debug when build breaks because it’s just small Elixir tasks
  • Cons:
    • Minimal features & magic
    • Verbose especially for small tasks
    • Some tools don’t have command line interfaces to call
    • Needs more testing on Windows

I hope it can be useful to someone or at least can inspire for better Mix helpers in the future (since the code is really not that nice). I use it in my own projects and it seems to work well for me, and now at least I can only blame myself if something goes wrong in the build. If you use it for something, please let me know any feedback you have in this thread. I will also use this thread to notify if I update MBU with new stuff.

9 Likes

I like the concept! I think this is valuable not only for building JS/CSS, but any front end assets in general, such as Elm.

What is the downside of this approach rather than with the basic Brunch configuration that comes with Phoenix?

3 Likes

That looks pretty good!

I had the idea to build this a while back as well (mentioned in some old build post her) but lack of time meant I never got around to testing the idea, I am so glad someone else did! ^.^

But yeah, this is something that elixir definitely needed, some method of just calling down to the raw OS like that, however I think you could make it more generic, it could be made into a runner to run any pre-builder for if you need to generate files for anything, not just the front-end, the only thing about it that I see that does not meet that already is just the name. ^.^

3 Likes

Agree with everyone above, this looks great :023:

Also interested in your answer to:

2 Likes

When I was installing vue-brunch after having uninstalled elm-brunch a while ago I was wondering why I can’t just run whichever tools I already have on the box (admittedly I have this attitude because I’m not confident enough with these web tools to step outside of these conventional tools/methods). This hits that dead center. I’ll second that it seems that the scope is way bigger than just building frontend stuff, so maybe it should be refocused/renamed into something like mix make?

3 Likes

Hey, thanks for the comments. :slight_smile:

It’s a very DIY tool. Brunch offers you the basic setup builtin and you just need to fill in the details. So if your usage patterns match Brunch’s, it’s quick to set up, I think (I haven’t done it from scratch). Whereas with FBU you need to write everything yourself, all the calls to the Node tools with the right CLI switches, copying things around, and thinking about how to split the tasks and what kind of hierarchy you want for them. It ends up being many files and can feel like a lot of code. On the other hand, you get complete freedom to do whatever you want.

Heh, you’re both right that it’s not limited to the frontend, you can build anything you want. I’m not sure about a name with as big implications as Mix Make. :sweat_smile: I named it FBU because that’s my usage for it, but I’ll consider other names. If you have good suggestions, I’d like to hear them, because I’m really bad at naming!

2 Likes

I think most programmers are, heck look at my named things, they are abysmally named… ^.^;

2 Likes

Well, I changed the name to MBU: Mix Build Utilities and put it on hex.pm: https://hex.pm/packages/mbu

Hope someone finds it useful for something. :slight_smile:

5 Likes

Awesome! This could become a good base for a few things. :slight_smile:

3 Likes

First post has been updated with the latest info :slight_smile:

3.0.0 release brings automatic folder handling, so that you don’t need to write your own out_path/0 for tasks that store things in temporary output folders.

5 Likes