Packaging your ExpressionEngine add-ons (the easy way)

19th September, 2011

One task which is often overlooked when creating ExpressionEngine add-ons is packaging the final result, to distribute to your users. While this may seem like a fairly insignificant task, there are some easy steps you can take to ensure your users get a consistently great package every time.

In nearly every case, you are going to be distributing your add-on as a *.zip file. While most operating systems come with built-in software to create zip files, there are several things we want to avoid:

.DS_Store and __MACOSX files
If you are like the majority of ExpressionEngine developers, you are probably using Mac OSX. Unfortunately, OSX has a nasty habit of creating hidden “.DS_Store” files everywhere on your file system. Depending on your settings, you may or may not be able to see these. However, when you create zip files using the built-in compress functionality in the Finder, these files are included in your zip. When your zip is extracted on another computer, the files are re-created. On Windows computers, these appear as regular files. When your users upload the directory to their server, all these files will be included, adding unnecessarily to the upload time. Finally, they are just plain ugly. We therefore would like to avoid unnecessary hidden files appearing in the final package.

Git and SVN files
Your source control system is also a source of hidden files you do not want in the final package. While SVN tends to leave hidden “.svn” directories littered throughout your filesystem, Git at least stores these files only in the root of your package directory. As well as the “.git” directory, there are also probably other hidden files you do not need to distribute to your users, such as “.gitignore”. The simple solution to this is to make a copy of your add-on directory, then delete all the files you don’t want before zipping it up. However, this is a fairly manual task, and if you forget to delete specific files, you may end up distributing private source control files to your users.

Version numbers
Another common issue is packages without a version number. When your users quickly scan down their downloads directory, they probably want to see what version they have downloaded. It is polite to include the version number of a package, just as you gave your package a descriptive name.

The problem

Let’s look at an example. The brilliant Mountee module (which we use, and highly recommend) is distributed as a zip file, “MounteeModule.zip”. Already, I do not know which version I have downloaded, and my downloads folder currently contains many versions I have downloaded over the months, with names such as “MounteeModule (3).zip”. This is obviously not ideal. In addition, let’s take a look at the contents of the zip:

unzip -l MounteeModule.zip 
Archive
:  MounteeModule.zip
  Length     Date   Time    Name
 
--------    ----   ----    ----
        
0  08-16-11 07:46   MounteeModule/
     
6148  08-16-11 07:46   MounteeModule/.DS_Store
        0  08
-16-11 07:46   __MACOSX/
        
0  08-16-11 07:46   __MACOSX/MounteeModule/
       
82  08-16-11 07:46   __MACOSX/MounteeModule/._.DS_Store
        0  04
-13-11 09:12   MounteeModule/EE1 Module/
     
6148  08-05-11 07:57   MounteeModule/EE1 Module/.DS_Store
        0  08
-16-11 07:46   __MACOSX/MounteeModule/EE1 Module/
       
82  08-05-11 07:57   __MACOSX/MounteeModule/EE1 Module/._.DS_Store
        0  04
-13-11 09:12   MounteeModule/EE1 Module/system/
     
6148  08-05-11 07:57   MounteeModule/EE1 Module/system/.DS_Store
        0  08
-16-11 07:46   __MACOSX/MounteeModule/EE1 Module/system/
       
82  08-05-11 07:57   __MACOSX/MounteeModule/EE1 Module/system/._.DS_Store
        0  08
-01-11 05:33   MounteeModule/EE1 Module/system/language/
     
6148  08-05-11 02:38   MounteeModule/EE1 Module/system/language/.DS_Store
        0  08
-16-11 07:46   __MACOSX/MounteeModule/EE1 Module/system/language/
       
82  08-05-11 02:38   __MACOSX/MounteeModule/EE1 Module/system/language/._.DS_Store
        0  12
-15-10 05:44   MounteeModule/EE1 Module/system/language/english/
      
162  12-15-10 05:44   MounteeModule/EE1 Module/system/language/english/lang.mountee.php 

This continues. In total, there are 80 entries in the zip file. 30 of these are the actual module’s files and folders, and 50 of these are unnecessary hidden OS files. While you may argue about the relative merits of saving a few kb, nearly half of the total size of the zip file is taken up by hidden files.

Our solution

While there are obviously many solutions to these problems, including manually keeping track of everything, we like to automate the process as much as possible. This ensures a consistently good package every time, and means we can release a new version with a single command. If you are using Mac OSX, or any unix-like operating system, chances are you have a handy little program called “Make” installed (on OSX it is installed as part of the Xcode package). Make is great for performing repetitive tasks related to building and packaging applications.

Let’s jump right into an example. For Store, our Makefile looks like this:

BUILD_VER := $(shell php -"require 'system/expressionengine/third_party/store/config.php'; echo STORE_VERSION;")

alldist
clean
:
    
rm -rf store-*
distclean
    
for i in `find system -name "*.php"`; do \
      php 
-$$i\
      
if $$? -ne 0 ] then exit fi \
    done
    zip 
-r store-$(BUILD_VER).zip system themes -"*/.*" "*/tests*" 

This simply needs to be placed in a file called “Makefile”, in the root of your add-on directory. To run it, simply type “make” at the command line, and a zip is created in the current directory, with the correct version number, and sans any annoying hidden files. Let’s go through this line by line.

BUILD_VER := $(shell php -"require 'system/expressionengine/third_party/store/config.php'; echo STORE_VERSION;"

This runs the specified PHP file, captures our STORE_VERSION constant, and stores it in a variable called BUILD_VER for later use. If your module supports NSM Add-on updater, you probably already have a config.php file. Simply modify this line to grab your current version number automatically.

alldist 

This states that the default target when typing “make” is to run “dist”. You can have multiple “targets” in your Makefile, however we only have “clean” and “dist”.

clean:
    
rm -rf store-* 

Next we define the “clean” target. It simply deletes any packages which have been previously created using make.

distclean 

Now we define the “dist” target. We also state here that “clean” will automatically be called before this target, to delete any existing packages created.

for i in `find system -name "*.php"`; do \
    php 
-$$i\
    
if $$? -ne 0 ] then exit fi \
done 

This clever little snippet of code will recursively find any PHP files in the current directory, loop through them, and run them through the PHP interpreter, to ensure they don’t have any syntax errors. While this should not replace good code testing on your behalf, it is a good final measure to ensure that you never distribute packages to your users with PHP syntax errors.

zip -r store-$(BUILD_VER).zip system themes -"*/.*" "*/tests*" 

The final step is to zip up the add-on. In this line, we create a new zip file named using our BUILD_VER variable we captured earlier. In the zip file we place the “system” and “themes” folders from the current directory. We also specifically exclude (-x) any files beginning with a period (.), or named “tests” (as it is unnecessary to distribute our tests to end users). You can add as many exclusions to the list as you like.

Voila! We now have a completely automated packaging process, with only a few extra lines of code. We can now package our add-on by typing “make”, and rest assured that no miscellaneous source control or OS hidden files will be included. It also means that we can easily re-create packages of previous versions, by simply checking out the required version in Git, and typing “make”.

It is worth mentioning that if your add-on is hosted by Github, it is also fairly simple to download a zip of your entire source directory directly. While this is a great solution for smaller add-ons, often there are extra files & folders you wish to exclude from the zip (such as “.gitignore” and “tests”). Using Make also allows you to run simple tests on your code, as we have demonstrated above, to ensure you create professional packages every time.

Do you have any tips to add when it comes to packaging add-ons? Which method do you currently use?