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 therun/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.
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:
- how to define a mix task
- Phoenix is divided into two parts: the installer and the rest
- 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