Robot Has No Heart

Xavier Shay blogs here

A robot that does not have a heart

Building Firefox Extensions

This article will introduce the basics of Ruby Rant by creating a Rantfile to build Firefox extensions. You don’t actually need to know anything about extensions to follow along, but if you are interested may I recommend this tutorial by roachfiend. You will note that that article (and many others on the same topic) use a batch file to build their extensions. While this is quick to set up for simple development, a build file saves time and effort in the long run, and gives more flexibility.

I assume you at least know what Rant is – a replacement for Rake – and have it installed and working. Please visit their website for more information on this topic. This is also not a build file tutorial – you should know what a task and a dependency are.

Table of Contents

  1. Extension Basics
  2. Rant
  3. Making the JAR
  4. Cleaning
  5. Making the XPI
  6. Final Touches
  7. The Completed Rakefile

Extension Basics

The first step is to decide on directory structure for your project. Firefox extensions are comprised of two main portions – the install instructions, and the actual content of the extension. A Firefox extension (an XPI file) is really just a zip file with a different extension. You can open it up using your favourite archive manager and see the following structure:

1
2
3
4
5
6
myextension.xpi/
  install.js
  install.rdf
  chrome/
    myextension.jar/
      ... myextension content ...

Likewise, the JAR file is also a zip file with an alternate extension. We can see that there are two major portions of the extension that need building, the JAR and the XPI (which contains the JAR). As such, we will use a source structure that looks like this (download the source code):

1
2
3
4
5
myextension/
  Rantfile
  src/
    install/
    jar/

Clearly, the install folder will only contain our install.js and install.rdf files, and the jar folder will contain the contents of our jar.

Rant

Enough introduction, let’s get started with Rant. Rant is a replacement for Rake. I won’t go into detail here, but one of the advantages for our purposes is portable zip creation without the need for external libraries. Rant is similar to Rake in that you define all your build tasks in a file in your root directory – the Rantfile. We will create 3 tasks – package, clean, and clobber. The first obviously packages up our extension into a zip file and gives it a .xpi extension. “clean” removes temporary files used to package the extension, and “clobber” removes all generated artefacts (basically the same as clean but also removes the XPI file).

Making the JAR

Baby steps steps though – first of all we want to create the JAR file for our extension. We can do this using the Archive::Zip generator provided by Rant:

1
2
3
4
5
6
7
import "archive/zip"
require "archive_rootdir_fix"

gen Archive::Zip, "build/helloworld", 
                  :files     => sys["src/jar/**/*"],
                  :rootdir   => "src/jar",
                  :extension => ".jar"

This generator creates a task called “build/helloworld.jar” that creates exactly that archive, containing all the files from src/jar. “**/*” tells rant to recursively add all files. The rootdir parameter is necessary so that the generator knows where to start adding files. Without it, the created JAR will have the “src/jar” folders inside it, which is undesirable.

I draw your attention to the archive_rootdir_fix file that is being required. Support for the rootdir parameter is currently not in Rant. I’ve submitted a patch, but until it is accepted, you need this particular file. It is included in the example source code for you convenience.

The generated task name is quite cumbersome, but it is quite trivial to create an alias to it using a blank task with a sole dependency. But what happens when we change our extension name or build directory? We also have to recode our alias task. Thankfully, the generator returns an object with information about the generated task, so that we can use it later in our Rantfile:

1
2
3
4
5
6
7
8
import "archive/zip"

jar_t = gen Archive::Zip, "build/helloworld", 
                  :files     => sys["src/jar/**/*"],
                  :rootdir   => "src/jar",
                  :extension => ".jar"

task :build_jar => jar_t.path

Cleaning

Before we proceed, let us quickly set up our clean and clobber tasks, as they are required for the next section. Rant makes this trivially easy, so I’m just going to show you some code and move on.

1
2
3
4
5
6
7
8
import "clean"

gen Clean, :clean
var[:clean] << "build"

gen Clean, :clobber
var[:clobber] << "build"
var[:clobber] << "bin"

Making the XPI

As you can imagine, the next step – packaging up the XPI file – is more of the same. A small amount of trickery is required to get the JAR file into the chrome directory – we actually move files around and prepare the XPI file in the build directory, so that our zip task only has to zip the single directory. You can do this using methods of the sys object. Since it uses standard shell commands it is fairly self explanatory, as you’ll see in the following example. See that we can keep using the jar_t object through out build file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xpitask = gen Archive::Zip, "bin/helloworld",
                            :version   => "1.0.0",
                            :files     => sys["build/**/*"],
                            :rootdir   => "build",
                            :extension => ".xpi"
task :build_xpi => xpitask.path           

task :prepare => [:build_jar] do |t|
  sys.mkdir_p "build/chrome"
  sys.mv jar_t.path, "build/chrome/helloworld.jar"
  sys.cp sys["src/install/**/*"], "build"
end

task :package => [:prepare, :build_xpi]

Note that we’ve added a version parameter to the zip task – this automatically appends a version string to our output file.

Final Touches

Now we just need to add the finishing touches to our build file. For maintainability, we will extract common names (such as the “helloworld” title and the “build” directory) into variables, so that changing them once will change them throughout the entire buildfile. You can use normal ruby variables for this, but it is preferable to use the “var” construct since it means you have the option of using them in Command generators later on (maybe I will cover it in another tutorial). It is more verbose, however, so you may choose not to use it in your own projects.

Finally, we move our public tasks to the top of file for readability and give them descriptions so they are displayed when executing “rant -T”. And there you have it folks, an automated build script for firefox extensions. Please download the source code to peruse at your leisure.

The Completed Rantfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# Rantfile for building Firefox Extension
# Xavier Shay (xshay@rhnh.net), July 2006

import "archive/zip"
require "archive_rootdir_fix"
import "clean"

# Configuration
var :title   => "helloworld"
var :version => "1.0.0"
var :build_dir => "build"
var :bin_dir => "bin"
var :src_dir => "src"

# Primary tasks
desc "Package up the XPI file for release"
task :package => [:prepare, :build_xpi]

desc "Cleanup temporary files"
gen Clean, :clean
var[:clean] << "build"

desc "Cleanup all generated artifacts"
gen Clean, :clobber
var[:clobber] << "build"
var[:clobber] << "bin"

# Support tasks
jar_t = gen Archive::Zip, "#{var :build_dir}/#{var :title}", 
                  :files     => sys["#{var :src_dir}/jar/**/*"],
                  :rootdir   => "#{var :src_dir}/jar",
                  :extension => ".jar"
task :build_jar => jar_t.path

xpi_t = gen Archive::Zip, "#{var :bin_dir}/#{var :title}",
                  :version   => "#{var :version}",
                  :files     => sys["#{var :build_dir}/**/*"],
                  :rootdir   => "#{var :build_dir}",
                  :extension => ".xpi"
task :build_xpi => xpi_t.path           

task :prepare => [:clean, :build_jar] do |t|
  sys.mkdir_p "#{var :build_dir}/chrome"
  sys.mv jar_t.path, "#{var :build_dir}/chrome/#{var :title}.jar"
  sys.cp sys["#{var :src_dir}/install/**/*"], "#{var :build_dir}"
end
A pretty flower Another pretty flower