To me the idea of contributing to a popular open source project, feels exciting and intimidating at the same time, since I’ve never done that before (except for improving the documentation/guides) and not sure know where to start.

But a few hours ago, my first commit to the Phoenix framework got merged, which made me technically a contributor. I just updated the version of Bootstrap in a newly generated Phoenix app to the latest (3.3.5 as of today). I understand it was just a minor change, but the reasoning process in figuring out how to do it with very little initial understanding of the internals of the Phoenix framework was still worth noting. I suppose there are other people who are interested in contributing to open source, but like me, have no idea how to start. So I’m sharing my experience, in the hope of benefiting other new developers and also myself.

The Start

Recently I got serious in learning the Phoenix framework, and was building a new website with it. While styling the home page, I noticed the version of Bootstrap was still 3.1.1, which was released in Feb 2014. So I think maybe it’s time to update it to the latest version.

OK! Let’s do it!

The (Boring) Details

First I need to figure out where the Bootstrap source file was located in the Phoenix project. Now I understand that in a new Phoenix app, the web/static/css/app.css file is where Bootstrap lives in and a new Phoenix app could be generated by running mix phoenix.new. This is a mix task you get from installing Phoenix. So there must be some clue in what this task would do, which should involve copying files from the Phoenix to a newly generated app. Let’s look into that. I already know mix is a task runner (and more) for Elixir, just like Rake is to Ruby and we can add new tasks for it to run. But unfortunately, I don’t know yet how to define a mix task. Time to consult the documentation for Mix task:

A simple module that provides conveniences for creating, loading and manipulating tasks.

A Mix task can be defined by simply using Mix.Task in a module starting with Mix.Tasks. and defining the run/1 function:

defmodule Mix.Tasks.Hello do
 use Mix.Task
 def run(_) do
   Mix.shell.info "hello"
 end
end

The run/1 function will receive all arguments passed to the command line.

OK. So to define a new Mix task, just define a module with a name prefix “Mix.Tasks”, use the Mix.Task module in it, and provide a run/1 function. According to the convention of a Mix project, the source code of a project usually lives in the lib directory. Let’s take a look there:

phoenix_source git:(master) ll lib
total 8
drwxr-xr-x   4 wiser  staff   136B Oct 16 11:06 mix
drwxr-xr-x  29 wiser  staff   986B Oct 16 11:06 phoenix
-rw-r--r--   1 wiser  staff   1.3K Oct 16 11:06 phoenix.ex
phoenix_source git:(master) ll lib/mix
total 16
-rw-r--r--   1 wiser  staff   5.1K Oct 16 11:06 phoenix.ex
drwxr-xr-x  11 wiser  staff   374B Oct 16 11:06 tasks
phoenix_source git:(master) ll lib/mix/tasks
total 88
-rw-r--r--  1 wiser  staff   984B Oct 16 11:06 compile.phoenix.ex
-rw-r--r--  1 wiser  staff   1.8K Oct 16 11:06 phoenix.digest.ex
-rw-r--r--  1 wiser  staff   1.4K Oct 16 11:06 phoenix.gen.channel.ex
-rw-r--r--  1 wiser  staff   4.6K Oct 16 11:06 phoenix.gen.html.ex
-rw-r--r--  1 wiser  staff   2.9K Oct 16 11:06 phoenix.gen.json.ex
-rw-r--r--  1 wiser  staff   6.3K Oct 16 11:06 phoenix.gen.model.ex
-rw-r--r--  1 wiser  staff   707B Oct 16 11:06 phoenix.gen.secret.ex
-rw-r--r--  1 wiser  staff   1.1K Oct 16 11:06 phoenix.routes.ex
-rw-r--r--  1 wiser  staff   829B Oct 16 11:06 phoenix.server.ex

Looks like I’m in the right place! A lot of the tasks of the Phoenix framework are defined here. But if you look closer, you would notice the phoenix.new task, the one I’m looking for, is missing here. So this is actually NOT the right place unfortunately. But it has to be defined somewhere in the project, so how about search for Mix.Tasks:

phoenix_source git:(master) ag --elixir -l "Mix.Tasks"
installer/lib/phoenix_new.ex
lib/mix/tasks/phoenix.digest.ex
lib/mix/tasks/phoenix.gen.channel.ex
lib/mix/tasks/phoenix.gen.json.ex
lib/mix/tasks/compile.phoenix.ex
lib/mix/tasks/phoenix.gen.secret.ex
lib/mix/tasks/phoenix.gen.html.ex
lib/mix/tasks/phoenix.routes.ex
lib/mix/tasks/phoenix.gen.model.ex
installer/test/phoenix_new_test.exs
lib/mix/tasks/phoenix.server.ex
test/mix/tasks/phoenix.digest_test.exs
test/mix/tasks/phoenix.gen.channel_test.exs
test/mix/tasks/phoenix.routes_test.exs
test/mix/tasks/phoenix.gen.secret_test.exs
test/mix/tasks/phoenix.new_test.exs
test/mix/tasks/phoenix.gen.model_test.exs
test/mix/tasks/phoenix.gen.json_test.exs
test/phoenix/code_reloader_test.exs
test/mix/tasks/phoenix.gen.html_test.exs

I can safely ignore the occurrences in lib and in tests, which leaves only installer/lib/phoenix_new.ex.

defmodule Mix.Tasks.Phoenix.New do
  use Mix.Task
  ...
end

This time I’ve found the definition for the phoenix.new task. The code is pretty complicated for a beginner, but I just want to the origin of the web/static/css/app.css file in a new Phoenix app and there is only one occurrence of the file path web/static/css/app.css:

@brunch [
    {:text, "static/brunch/.gitignore",       ".gitignore"},
    {:eex,  "static/brunch/brunch-config.js", "brunch-config.js"},
    {:text, "static/brunch/package.json",     "package.json"},
    {:text, "static/app.css",                 "web/static/css/app.css"},
    {:eex,  "static/brunch/app.js",           "web/static/js/app.js"},
    {:eex,  "static/brunch/socket.js",        "web/static/js/socket.js"},
    {:text, "static/robots.txt",              "web/static/assets/robots.txt"},
  ]

So this means the original file is named app.css in directory static. Since there isn’t a static directory directly under installer:

phoenix_source git:(master) ll installer
total 16
-rw-r--r--  1 wiser  staff   184B Oct 16 11:06 README.md
drwxr-xr-x  3 wiser  staff   102B Oct 17 08:28 _build
drwxr-xr-x  3 wiser  staff   102B Oct 16 11:06 lib
-rw-r--r--  1 wiser  staff   303B Oct 16 11:06 mix.exs
drwxr-xr-x  5 wiser  staff   170B Oct 16 11:06 templates
drwxr-xr-x  5 wiser  staff   170B Oct 16 11:06 test

let’s just search for the file name:

phoenix_source git:(master) cd installer
installer git:(master) find . -name "app.css"
./templates/static/app.css

Since there is only one hit, it must be the original file I’m looking for. I can easily confirm that by comparing this file and the web/static/css/app.css file in a new Phoenix app.

Actually editing the file is really simple: just delete the older version of Bootstrap in installer/templates/static/app.css and paste in the latest version of Bootstrap. And of course make sure the tests still pass and verify the home page of a new Phoenix app still looks the same as before.

Verification

You can run the tests by mix test in root directory of Phoenix source code, which is pretty simple. But for verifying the home page after my updates, I need to briefly review how Phoenix was installed. As of today, the latest version of Phoenix is 1.0.3 and according to the Installation Guide, it can be installed by running:

mix archive.install https://github.com/phoenixframework/phoenix/releases/download/v1.0.3/phoenix_new-1.0.3.ez

After installing Phoenix, a new mix task phoenix.new would be available in any directory on your system, but other mix tasks provided by Phoenix like phoenix.gen.html or phoenix.routes wouldn’t be available until a Phoenix application is generated and they are only available in the root directory of that application. That means the ez archive you installed is only responsible for bootstrapping a new Phoenix application, namely providing a new command for generating it, as well as providing all the static assets and configuration files required. But the Phoenix framework itself is not included in the archive and is only added as a dependency in the mix.exs file of a newly generated application. So I can guess by now, the installer archive is built from the code in installer directory. Let’s confirm that by looking at the mix.exs file in installer directory:

defmodule Phoenix.New.Mixfile do
  use Mix.Project

  def project do
    [app: :phoenix_new,
     version: "1.0.3",
     elixir: "~> 1.0-dev"]
  end

  ...
end

As we can see the project name is phoenix_new and version is 1.0.3, which exactly match the file name of the installer archive phoenix_new.1.0.3.ez. Let’s try to build this archive with my updates:

installer git:(master) mix archive.build
Generated archive "phoenix_new-1.0.3.ez" with MIX_ENV=dev
installer git:(master) ll
total 312
-rw-r--r--  1 wiser  staff   184B Oct 16 11:06 README.md
drwxr-xr-x  3 wiser  staff   102B Oct 19 21:52 _build
drwxr-xr-x  3 wiser  staff   102B Oct 16 11:06 lib
-rw-r--r--  1 wiser  staff   303B Oct 16 11:06 mix.exs
-rw-r--r--  1 wiser  staff   144K Oct 19 21:52 phoenix_new-1.0.3.ez
drwxr-xr-x  5 wiser  staff   170B Oct 16 11:06 templates
drwxr-xr-x  5 wiser  staff   170B Oct 16 11:06 test

As expected, the installer archive is built like this, so my guess was confirmed.

According to the documentation of Mix.Tasks.Archive, archives are by default installed at ~/.mix/archives, and that’s why mix tasks from the installed archives are available system wide. We can list the archives currently installed by:

~  mix archive
* hex-0.9.0.ez
* phoenix_new-1.0.3.ez
Archives installed at: /Users/wiser/.mix/archives

Let’s install this newly built archive:

installer git:(master) ✗ mix archive.uninstall phoenix_new-1.0.3.ez
installer git:(master) ✗ mix archive.install
Found existing archive(s): phoenix_new-1.0.3.ez.
Are you sure you want to replace them? [Yn]
* creating /Users/wiser/.mix/archives/phoenix_new-1.0.3.ez

Note that I uninstalled the existing “standard” installer archive before installing the one I just built. Then generate a new Phoenix application and get it running:

elixir mix phoenix.new sample
cd sample
sample mix deps.get
sample mix phoenix.server

Let’s first check the version of web/static/css/app.css in this new application:

/*!
 * Bootstrap v3.3.5 (http://getbootstrap.com)
 * Copyright 2011-2015 Twitter, Inc.
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 */ ...

So Bootstrap is updated as expected. Only need to verify that everything still looks normal. I kept a new application generated by the standard installer and compare with the sample application I just generated with my new archive. And they look exactly the same.

Now everything is good. Commit the changes, push to my fork of the Phoenix framework, and create a pull request. :tada:

Retrospect

This was a very small change, but along the whole process, I’ve gained so much more understanding about how Phoenix and Mix works. I now know:

  1. how to define a mix task
  2. Phoenix is divided into two parts: the installer and the rest
  3. How to work with archives, namely list, build, install and uninstall

I belief this new knowledge will be helpful for using Phoenix as well as for making more contributions to it.

Leave a comment