All software built in Conary is controlled through a recipe, which is essentially a Python module with several characteristics. Here is an example recipe, which has a typical complexity level:
class MyProgram(PackageRecipe):
name = 'myprogram'
version = '1.0'
buildRequires = [ 'somelib:devel' ]
def setup(r):
r.addArchive('http://example.com/%(name)s-%(version)s.tar.gz')
r.Configure()
r.Make()
r.MakeInstall()
The goal of Conary's recipe structure is not to make all packaging trivial, but to make it possible to write readable and maintainable complex recipes where necessary, while still keeping the great majority of recipes extremely simple—and above all, avoiding boilerplate that needs to be copied from recipe to recipe. This example is truly representative of the the most common class of recipes; the great majority of packaging tasks do not require any further knowledge of how recipes work. In other words, this example is representative, not simplistic. New packagers tend to find it easy to learn to write new Conary packages.
However, some programs are not designed for such easy packaging, and many packagers have become used to the extreme complexity required by some common packaging systems. This experience can lead to writing needlessly complex and thereby hard-to-maintain recipes. This, in turn, means that while reading the RPM spec file or Debian rules for building a package can be an easy way to find a resolution to a general packaging problem when you are writing a Conary recipe, trying to translate either of them word-by-word is likely to lead to a poor Conary package.
The internal structure of objects that underlie Conary recipes makes them scale gracefully from simple recipes (as in the example) to complex ones (the kernel recipe includes several independent Python classes that make it easier to manage the kernel configuration process). Some of the more complex recipe possibilities require a deeper structural understanding.
The recipe module contains a class that is instantiated as the recipe object (MyProgram in the example above). This class declares, as class data, a name string that matches the name of the module, a version string, and a setup() method. This class must be a subclass of one of a small family of abstract superclasses (such as PackageRecipe).
Conary calls the recipe object's setup() method, which populates lists of things to do; each to-do item is represented by an object. There are source objects, which represent adding an archive, patch, or source file; and build objects, which represent actions to take while building and installing the software. Additionally, there are pre-existing lists of policy objects to which you can pass extra information telling them how to change from their default actions. The setup() function returns after preparing the lists, before any build actions take place.
Conary then processes the lists of things to do; first all the source objects, then all the build objects, and finally all the policy objects.
It is important to keep in mind that unlike RPM spec files and portage ebuild scripts (processed in read-eval-print loop style by a shell process) or Debian rules (processed by make), a Conary recipe is processed in two passes (three, if you count Python compiling the source into bytecode), because it both constrains the actions you can or should take and makes Conary more powerful. For example, you should not add sources inside a Python conditional (instead, you unconditionally add them but can choose not to apply them based on a conditional), but this constraint allows Conary to always automatically store copies of all sources that it has fetched by URL instead of being explicitly committed locally.
Another important data structure in a recipe is the macros object, an enhanced dictionary object that is an implicit part of every recipe. Almost every string used by any of the different kinds of objects in the recipe—including the strings stored in the macros object itself—is automatically evaluated relative to the contents of the macros object, meaning that standard Python string substitution is done. Thus, you do not have to type %r.macros after every string; the substitution is done within the functions you call. It also means that macros can reference each other. Be aware that changes to the macros object all take place before any list processing. This means that an assignment or change to the macros object at the end of the recipe will affect the use of the macros object at the beginning of the recipe. This is an initially non-obvious result of the multi-pass recipe processing.
The string items contained in the macros object are colloquially referred to by the Python syntax for interpolating dictionary items into a string. Thus, r.macros.foo is usually referred to as %(foo)s, because that is the way you normally see it used in a recipe.
The macros object contains a lot of immediately useful information, including the build directory (%(builddir)s), the destination directory (%(destdir)s) that is the proxy for the root directory (/) when the software is installed, many system paths (%(sysconfdir)s for /etc and %(bindir)s for /usr/bin), program names (%(cc)s), and arguments (%(cflags)s).
Conary recipes can reference each other, which makes it easier to use them to create a coherent system.
When many packages are similar, it is easy to end up with boilerplate text that is copied between packages to make the result of cooking them reflect that similarity. That boilerplate can be encoded in PackageRecipe subclasses, stored in recipes that are normally never cooked because they function as abstract superclasses. The recipes containing those abstract superclasses are loaded with the loadSuperClass() function, which loads the latest version of the specified recipe from the repository into the current module's namespace. The main class in the recipe then descends from that abstract superclass. (The inheritance is pure Python, so it is possible to use multiple inheritance if that is useful.) This mechanism serves two main purposes: it reduces transcription errors in what would otherwise be boilerplate text, and it reduces the effort required to build similar packages. It also allows bug fixes that are generic to be made in the superclass and thus automatically apply to all the subclasses. Finally, build requirements of superclasses are automatically applied to subclasses.
Sometimes, you want to reference a recipe without inheriting from it. In that case, you use a similar function called loadInstalled(), which loads a recipe while preferring the version that is installed on your system, if any version is installed on your system. (Otherwise, it acts just like loadSuperClass().) For example, you can load the perl recipe in order to programmatically determine the version of perl included in the distribution, without actually requiring that perl even be installed on the system.
Conary has several abstract superclasses that you can use without calling the loadSuperClass() function. They provide common build requirements, and in some cases, also some default actions. These superclasses are currently built directly into Conary.
Requires the essential dependencies to use Conary to package the software; the filesystem layout, Conary, and Conary's packaging requirements (python, sqlite, gzip, bzip2, tar, cpio, and patch).
Requires basic shell utilities (coreutils), mktemp, make, and the programs needed to run configure (findutils, gawk, grep, sed, and diffutils). It inherits from PackageRecipe and therefore includes all the build requirements of PackageRecipe.
Requires the compiler, tools (such as linker), and C library; all the tools required to compile and package C programs. It inherits from BuildPackageRecipe.
Designed to provide minimal recipes for programs packaged with the autoconf, automake, and libtool tools, this superclass descends from CPackageRecipe and implements several methods: unpack(), configure(), make(), makeinstall(), and policy(), called in that order from the built-in setup() method. The unpack() method is the only method that must always be implemented in a subclass of AutoPackageRecipe; the default behavior, which works for most correctly-packaged programs is to run r.Configure(), r.Make(), and r.MakeInstall().
This is a special superclass recipe with many constraints on its subclasses; each subclass will create exactly one user, and the user is created by a single call to the User() method. The full package name is info-foo and it will contain the single component info-foo:user, in order to create the user named foo.
This is a special superclass recipe very like UserInfoPackageRecipe, except that it creates groups rather than users. It has two methods; the Group() method creates a group that is not associated with a user, and the SupplementalGroup() method creates a group which is added to an existing user. The full package name is info-foo and it will contain the single component info-foo:group, in order to create the group named foo. Note that this means that if you have defined a user named foo, the only way to define a group with the exact same name foo is within the definition of the user foo; you cannot have both a info-foo:user and a info-foo:group definition on the same Conary label.
Given these extra superclasses, the myprogram recipe could reformed as follows:
class MyProgram(AutoPackageRecipe):
name = 'myprogram'
version = '1.0'
buildRequires = [ 'somelib:devel' ]
def unpack(r):
r.addArchive('http://example.com/%(name)s-%(version)s.tar.gz')
In its new form, it is three lines shorter, yet has a much more complete set of build requirements, thanks to inheritance.
The rPath repository has several useful superclasses on the conary.rpath.com@rpl:devel label. These superclasses have to be loaded with the loadSuperClass() function before they can be used.
loadRecipe('gnomepackage.recipe') allows you to use GnomePackageRecipe, which includes some basic Gnome build requirements (such as pkgconfig, gtk, zlib, and xorg-x11), and implements lots of generic functionality for building standard (or almost-standard) Gnome packages. Because Gnome has a standard location based on project name and version, GnomePackageRecipe works for many recipes without the subclasses defining any methods at all. GnomePackageRecipe defines the unpack(), build(), and install() methods.
loadRecipe('kdepackage.recipe') allows you to use KDEPackageRecipe, which acts just like GnomePackageRecipe does, except that it works for KDE instead of Gnome.
loadRecipe('cpanpackage.recipe') makes it easier to package Perl software from the CPAN archive by subclassing from CPANPackageRecipe. Unfortunately, CPAN is not as consistent as Gnome or KDE, so CPANPackageRecipe can take a little bit more work to use.