Skip navigation.
Home

"The scarily-named Injector"

The Zero Install Injector is named after the concept of "dependency injection". In this post, I'll try to explain what this means and why we use it.

Introduction

When you run a typical application, it's actually made up of several components, often developed by different groups of people. Let's start with a really simple example program ("About") that displays a dialog box telling the user some details of their system (kernel version number, etc):

#!/usr/bin/env python
import os, rox
rox.info("Your system: " + str(os.uname()))

It's not too important how it works, but the first line tells us it needs a Python interpreter to run it and the second says it uses the "os" and "rox" modules. The "rox" module, which is from ROX-Lib, in turn requires Python-GTK. We can draw these dependency relationships like this:

Dependencies for About

Unfortunately, this program is hopelessly naive. If you try to run it and you have ROX-Lib2 installed (by some traditional means, not using Zero Install) then it should run and display a simple information dialog box, but if not you'll get this unhelpful error:

Traceback (most recent call last):
  File "./about.py", line 2, in 
    import os, rox
ImportError: No module named rox

Apart from being overly technical, this error doesn't tell the user where to go to get the "rox" module. Worse, many desktop environments hide error messages from the user. If you try to run this file in GNOME, for example, it will just silently do nothing.

If we want to make user-friendly programs, we'll need to check that the user has ROX-Lib and, if not, explain to them how to get it (or offer to download it for them).

A wrong turn

One obvious way to do this is to add some code to our program that turns the above error into something more useful. In fact, ROX-Lib comes with a suitable file called findrox.py which we can bundle with the application. Our main program now looks like this:

#!/usr/bin/env python
import findrox; findrox.version(2, 0, 0)
import os, rox
rox.info("Your system: " + str(os.uname()))

Now when we run it we get a nice dialog box with this message:

*** This program needs ROX-Lib2 (version 2.0.0) to run.
I tried all of these places:

/home/talex/lib
/usr/local/lib
/usr/lib

ROX-Lib2 is available from:
http://rox.sourceforge.net

Likewise, ROX-Lib itself contains some code to find Python-GTK and to display a helpful error if it's not present. If we were really keen, we could wrap our program in a shell script that even checked that Python was installed before running the main program. Whenever one component depends on another, it must contain some code to find it, like this:

The findrox system

These techniques work (we've used them for several years), but not very well. Some problems:

  • Every program using ROX-Lib needs a copy of findrox.py. There are many old and buggy versions of this file still being supplied with programs, because they all need to be updated manually by each program's author.

  • For some people, the code isn't clever enough. For example, it doesn't offer to download ROX-Lib for you, and it doesn't let you choose which of several versions to use.

  • For others, it's too heavy-weight. If Debian packaged our About program then they wouldn't want findrox, because apt-get handles that.

  • The code for finding ROX-Lib is different to the code for finding Python-GTK, etc. Each one has its way of working (e.g. findrox looks for ~/lib/ROX-Lib2, but ROX-Lib doesn't look for ~/lib/Python-GTK).

We could improve the situation a bit by creating a standard library whose purpose was simply to find other components (in a generic way):

A generic finder

In this diagram, the user runs About, which uses the generic finder to locate Python and ROX-Lib. Then, ROX-Lib uses the finder to get Python-GTK. If it became widespread, it would remove some of the duplication and inconsistencies, but it's not a great solution. It's likely that different programmers would choose to use different finder modules, which would create a real mess. For example, the Python-GTK developers might use a different finder to locate Python-GTK's dependencies (such as GTK; not shown).

At first sight, it looks like this might be a general unsolvable program in computer science. But in fact, there's a solution that's simple, elegant and flexible...

Dependency injection

The term Dependency Injection was coined by Martin Fowler in 2004, although the concept is older. The basic idea is to reverse the relationship between the requiring components and the finder. Rather than having the user run About, and having About ask the finder to locate ROX-Lib, the user starts with the finder. The finder locates everything and then tells About where to find ROX-Lib and it tells ROX-Lib where to find Python-GTK:

A dependency injector

There are some interesting results of this design. No component (About, ROX-Lib, etc) has to know anything about the finder. The user chooses the finder, which can be as complex as one that downloads all the required packages, checks for updates and shares the code automatically (e.g. the Zero Install Injector, 0launch), or as primitive as the user manually ensuring everything is in the right place themselves.

In more concrete terms, the above is what happens when you do:

$ 0launch ~/apps/About.xml

Here, 0launch is the generic finder and About.xml is the configuration. However, since downloading and managing the configuration is itself a fair bit of work, we normally let 0launch handle that too:

$ 0launch http://example.com/About.xml

So, let's take a look at our final, dependency-injection-enabled version of About:

#!/usr/bin/env python
import os, rox
rox.info("Your system: " + str(os.uname()))

Yes, it's identical to the original naive version! It's up to the injector to tell each component about its dependencies in whatever is the most natural way for that component. In this case, it would ensure that the appropriate directories are listed in the $PYTHONPATH environment variable.

Of course, this isn't a particularly novel concept. You could easily argue that tranditional Linux installers such as apt-get are doing the same thing, by ensuring that everything is installed before the user starts the program, although they separate out the steps of installing and then running the program. Zero Install allows using this same style of programming for software that isn't distributed through centralised repositories. It also adds a few twists, such as deciding which version of ROX-Lib to use on a per-program basis (About might need an older version than Edit, for example) and allowing users to install without root access.

There are still a few places where we don't fully use this pattern. For example, when you click on "Edit MIME rules" in ROX-Filer's options box, ROX-Filer invokes 0launch explicitly to download and run the MIME-Editor (thus, this feature of ROX-Filer depends on 0launch). Ideally, ROX-Filer would just run "mime-editor" and 0launch would ensure that a suitable program was in $PATH. Currently, however, 0launch doesn't support lazy evaluation; it would insist on downloading MIME-Editor before letting you start ROX-Filer, which is (probably) not what we want.

For many cases though, 0launch already allows you to write applications that contain no code for dealing with dependencies, yet still support all the modern automatic download-and-update features users expect. For example, if you create a tarball containing just the single 3-line file above then you have a package that can be downloaded and used by 0launch! You just need to write the configuration saying what it needs (Python and Python-GTK are so common now that all you really need to list in the configuration is ROX-Lib).

Syndicate content