Disclaimer: Not all recipes written by rPath follow these best practices. We learned many of these best practices by making mistakes, and have not fully systematically cleaned up every recipe. So the first rule is probably not to worry about mistakes; Conary is pretty forgiving as a packaging system, and we have tried to make it fix mistakes when possible and warn about mistakes otherwise. You absolutely do not need to memorize this list to be a Conary packager. It's quite possible that the majority of new Conary recipes are fewer than ten lines of code. Relax, everything is going to be all right!
The best practices are somewhat arbitrarily divided into general packaging policy suggestions, conventions affecting building sources into binaries, and conventions affecting the %(destdir)s.
Before worrying about packaging details, start working on consistency at a higher level. Names of packages, structure of recipes, and versions are best kept consistent within a repository, and between repositories.
Starting simple: recipes are Python modules. Follow Python standards as a general rule. In particular, do not use any tabs for indentation. Tabs work fine, but you will be running the cvc diff command many times, and tabs make diff output look a little odd because the indentation levels are not all even, and when you mix leading tabs and leading spaces, the output looks even weirder.
Second, follow Conary standard practice. Conary standard practice has one significant difference from Python standard practice: the self-referential object is called r (for recipe) instead of self because it is used on practically every line.
To get the most benefit from Conary, write your recipes to make it easy to maintain a unified patch that modifies them. That way, someone who wants to shadow your recipe to make (and maintain) a few small changes will not be stymied. The most basic way to do this is to keep your recipe as simple as possible. Don't do anything unnecessary. Don't do work that you can count on policy to do for you, such as recompressing gzip or bzip2 files with maximal compression turned on, or moving files from /etc/rc.d/init.d to %(initdir)s, unless policy can't do the job quite the right way—and in that case, add a comment explaining why, so that the person shadowing your recipe does not walk into a trap.
Not doing lots of make-work has another important benefit. The less you do in the recipe, the less likely you are to clash with future additions to policy, and the more likely you are to benefit from those additions. Policy will continue to grow to solve packaging problems as we continue to find ways to reduce packaging problems to general cases that have solutions which we can reliably (partially or completely) automate.
Whenever possible, follow upstream conventions. Use the upstream name, favoring lower case if upstream sometimes capitalizes and sometimes does not, and converting ``-'' to ``_'' because ``-'' is reserved as a separator character. Do not add version numbers to the name; use version numbers in the name only if the upstream project indisputably uses the version number in the name of the project. Conary can handle multiple versions simultaneously just fine by using branches; no need to introduce numbers into the name. The branches can be created quite arbitrarily; they do not need to be in strict version order. Just avoid clashing with the label used for release stages by choosing a very package-specific tag. Example tags rPath has used so far are sqlite2, gnome14, and cyrus-sasl1. The head of conary.rpath.com@rpl:devel for sqlite is sqlite version 3, and the branch conary.rpath.com@rpl:devel/2.8.15-1/sqlite2/ contains sqlite version 2, as of this writing version 2.8.16, giving a full version string of /conary.rpath.com@rpl:devel/2.8.15-1/sqlite2/2.8.16-1
When possible and reasonable, one upstream package should produce one Conary package. Sometimes, usually to manage dependencies (such as splitting a text-mode version of a program from a graphical version so that the text-mode version can be installed on a system without graphical libraries installed) or installed features (such as splitting client from server programs), it is reasonable to have one upstream package produce more than one Conary package. Rarely, it is appropriate for two upstream packages to be combined; this is generally true only when the build instructions for a single package require multiple archives to be combined to complete the build, and all the archive files really are notionally the same project; they aren't just a set of dependencies.
If you have to convert ``-'' to ``_'' in the name, you can do something like this:
r.macros.ver = r.version.replace('_', '-')
You can then reference %(version)s for the version of the Conary package, and %(ver)s for the upstream version. In particular, you might need to do something like this:
r.addArchive('http://example.com/%(name)s-%(ver)s.tar.gz')
If the upstream name changes, change the name of the package as well. This means creating a new package with the new name, and then changing the old package into a redirect that points to the new package. Users who update the package using the old name will automatically be updated to the new package.
Alternatively, if you change the package that provides the same functionality, you can do the exact same thing; from Conary's point of view there is no difference. For example, rPath Linux used to use the old mailx program to provide /usr/bin/mail, but switched to the newer nail program for that task. The mailx recipe was then changed to create a redirect to the newer nail package. Anyone updating the mailx package automatically got the nail package instead.
A redirect is not necessarily forever. The old recipe for mailx could be restored and a new version cooked; the nail recipe could even be changed to a redirect back to mailx. If that happened, an update from the older version of mailx would completely ignore the temporary appearance of the redirect to nail. (Not that this is likely to happen—it just would not cause a problem if it did.)
Redirects pointing from the old troves to the new troves solve the ``obsoletes wars'' that show up with RPM packages. In the RPM universe, two packages can each say that they obsolete each other. In the Conary world, because redirects point to specific versions and never just to a branch or shadow, this disagreement is not possible; the path to a real update always terminates, and terminates meaningfully with a real update. There are no dead ends or loops.
Compiling software using consistently similar practices helps make developers more productive (because they do not have to waste time figuring out unnecessarily different and unnecessarily complex code) and Conary more useful (by enabling some of its more hidden capabilities).
Use build functions other than r.Run whenever possible, especially when modifying the %(destdir)s.
Build functions that do not have to start a shell are faster than r.Run. Using more specific functions enables more error checking; for example, r.Replace not only is faster than r.Run('sed -i -e ...') but also defaults to raising an exception if it cannot find any work to do. These functions can also remove build requirements (for example, for sed:runtime), which can make bootstrapping simpler and potentially faster.
Most build functions have enough context to prepend %(destdir)s to absolute paths (paths starting with the / character) but in r.Run you have to explicitly provide %(destdir)s whenever it is needed. For example, r.Replace('foo', 'bar', '/baz') is essentially equivalent (except for error checking) to r.Run("sed -i -e 's/foo/bar/g' %(destdir)s/baz") in function, but the r.Replace is easier to read at a glance.
Many build functions automatically make use of Conary configuration, including macros. r.Make automatically enables parallel make (unless parallel make has been disabled for that recipe with r.disableParallelMake), and automatically provides many standard variables.
A few build functions can actually check for missing buildRequires items. For example, if you install desktop files into the /usr/share/applications/ directory using the r.Desktopfile build command, it will ensure that desktop-file-utils:runtime is in your buildRequires list, and cause the build to fail if it is not there.
In particular, r.Configure, r.Make, and r.MakeParallelSubdirs provide options and environment variables that the autotools suite, and before that, the default make environment, have made into de-facto standards, including names for directories, tools, and options to pass to tools. Consistency isn't just aesthetic here; it also enhances functionality. It enables :debuginfo components that include debugging information, source code referenced by debugging information, and build logs. It allows consistently rebuilding the entire operating system with different compiler optimizations or even different compilers entirely. This is useful for testing compilers as well as customizing distributions.
Use macros extensively. In general, macros allow recipes to be used in different contexts, allow changes to be made in one place instead of all through a recipe, and can make the recipe easier to read.
Using macros for filenames means that a single recipe can be evaluated differently in different contexts. If you refer to the directory where initscripts go as %(initdir)s, the same recipe will work on any distribution built with Conary, whether it uses /etc/rc.d/init.d/ or /etc/init.d/.
Using macros such as %(cc)s for program names (done implicitly with make variables when calling the r.Make* build actions) means that the recipe will adapt to using different tools, whether that is for building an experimental distribution with a new compiler, or for using a cross-compiler to build for another platform, or any other similar purpose.
Use the macros that define standard arguments to pass to programs, such as %(cflags)s, and modify them intelligently. Instead of overwriting them, just modify them, like r.macros.cflags += ' -fPIC' or r.macros.cflags = r.macros.cflags.replace('-O2', '-Os') or r.macros.dbgflags = r.macros.dbgflags.replace('-g', '-ggdb')
Creating your own macros for text that you would otherwise have to repeat throughout your recipe makes the recipe more readable, less susceptible to bugs from transcribing existing errors and making errors in transcription, and easier to modify. You might, for example, do things like r.macros.prgdir = '%(datadir)s/%(name)'
Creating your own macros can also help you make your recipes fit in 80 columns, for easier reading in the majority of terminal sessions.
r.macros.url = 'http://reallyLongURL/'
r.addArchive('%(url)s/%(name)s-%(version)s.tar.bz2')
When configuring software (generally speaking, before building it, but it is also possible for configuration to control what gets installed rather than what is built), make sure that the configuration choices are represented in the flavor. When a configuration item depends on the standard set of Use flags for your distribution, use those. If there is no system-wide Use flag that matches that configuration item, you can create a local flag instead.
A lot of configuration is encoded in the arguments to r.Configure. We commonly use the variable extraConfig to hold those. There are two reasonable idioms:
extraConfig = ''
if Use.foo:
extraConfig += ' --foo'
r.Configure(extraConfig)
and
extraConfig = []
if Use.foo:
extraConfig.append('--foo')
r.Configure(' '.join(extraConfig))
In either case, referencing Use.foo will cause the system-wide Use flag named ``foo'' to be part of the package's flavor, with the value that is set when the recipe is cooked.
If you need to create a local flag, you do it with the package metadata (like name, version, and buildRequires):
class Asdf(PackageRecipe):
name = 'asdf'
version = '1.0'
Flags.blah = True
Flags.bar = False
def setup(r):
if Flags.blah:
...
Now the asdf package will have a flavor that references asdf.blah and asdf.bar. The values provided as metadata are defaults that are overridden (if desired) when cooking the recipe.
Choosing how to install files into %(destdir)s can determine how resilient your recipe is to changes in Conary and in upstream packaging, and how useful the finished package is.
Using r.Make('install') would not work very well, because it would normally cause the Makefile to try to install the software directly onto the system, and you would soon see the install fail because of permission errors. Instead, use r.MakeInstall(). It works if the Makefile defines one variable which gives a ``root'' into which to install, which by default is called DESTDIR (thus the %(destdir)s name). If that does not work, read the Makefile to see if it uses another make variable (common names are BUILDROOT and RPM_BUILD_ROOT), and pass that in with the rootVar keyword: r.MakeInstall(rootVar='BUILDROOT')
Sometimes there is no single variable name you can use. In these cases, there is a pretty powerful ``shotgun'' available: r.MakePathsInstall. It re-defines all the common autotools-derived path names to have the %(destdir)s prepended. This works for most of the cases without an install root variable. Sometimes you will find (generally from a permission error, less commonly from reviewing the changeset) that you need to pass an additional option: r.MakePathsInstall('WEIRDDIR=%(destdir)s/path/to/weird/dir')
For a few packages, there is no Makefile, just a few files that you are expected to copy into place manually. Use r.Install, which knows when to prepend %(destdir)s to a path, and knows that source files with any executable bit set should default to mode 0755 when packaged, and source files without any executable bit set should default to 0644; specify other modes like mode=0600—do not forget the leading 0 that makes the number octal. (Conary does look for mode values that are nonsensical modes and look like you left the 0 off, and warns you about them, but try not to depend on it; the testing is heuristic and not exhaustive.) Like other build actions, r.Install will create any necessary directories automatically. If you want to install a file into a directory, make sure to include a trailing / character on the directory name so that Install knows that it is intended to be a directory, not a file. (It is this requirement that allows it to make directories automatically.)
Conary does its best to make all packages multi-lib aware. Practically all 32-bit x86 libraries are available to work on the 64-bit x86_64 platform as well, making Conary-based distributions for x86_64 capable of running practically any 32-bit x86 application, not just a restricted set that uses some of the most commonly-used libraries.
The first thing to do is to always use %(libdir)s and %(essentiallibdir)s instead of /usr/lib and /lib, respectively. Furthermore, for any path that is not in one of those locations, but still has a directory named ``lib'' in it, you should use %(lib)s instead of lib. Conveniently, for programs that use the autotools suite, Conary does this for you, but when you are reduced to choosing directory names or installing files by hand, follow this rule.
On 64-bit platforms on which %(lib)s resolves to lib64, Conary tries to notice library files that are in the wrong place, and will even move 64-bit libraries where they belong, while warning that this should be done in the packaging rather than as a fixup, because there is probably other work that also needs to be done. Conary warns about errors that it cannot fix up, and causes the cook to fail.
Conary specifically ensures that :python and :perl components are multi-lib friendly, since there are special semantics here; some :python or :perl packages have only interpreted files and so should be found in the 32-bit library directory even on 64-bit platforms; others have libraries as well, and should be in the 64-bit library on 64-bit platforms. Putting the 64-bit object files in one hierarchy and interpreted files in another hierarchy would create path collisions between 32-bit and 64-bit :python or :perl components.
Occasionally, an upstream project will include a package of data files that is intended to be unpacked directly into the filesystem. Instead of unpacking it into the build directory with r.addSource and then copying it to %(destdir)s with r.Copy or r.Install, use the dir argument to r.addArchive. Normally, dir is specified with a relative path and thus is relative to the build directory, but an absolute path is relative to %(destdir)s. So something like r.addArchive('http://example.com/foo.tar.bz2', dir='%(datadir)s/%(name)s/') will do what you want in just one line. Not only does it make for a shorter recipe with less potentially changing text to cause shadow merges to require manual conflict resolution, it is also faster to do.
Most packaging rules tell you to use relative symlinks (../...) instead of absolute (/...) symlinks, because it allows the filesystem to continue to be consistent even when the root of the filesystem is mounted as a subdirectory rather than as the system root directory; for example, in some ``rescue disk'' situations.
This rationale is great, but Conary does something even better. It automatically converts all absolute symlinks not just to relative symlinks, but to minimal relative symlinks. That is, if you create the absolute symlink /usr/bin/foo -> /usr/bin/bar, Conary will change that to /usr/bin/foo -> bar, and /usr/bin/foo -> /usr/lib/foo/bin/foo to /usr/bin/foo -> ../lib/foo/bin/foo. Therefore, for Conary, it is best to use absolute symlinks in your %(destdir)s and let Conary change them to minimal relative symlinks for you.
Conary has a very strong dependency system, but it is a bit different from legacy dependency systems. The biggest difference is that depending on versions is very different from any other packaging system. Because Conary (by design) does not have a function that tries to guess which upstream versions might be newer than another upstream version, you cannot have a dependency that looks like ``upstream version 1.2.3 or greater.''
Because Conary has the capability for a rich branching structure, trying to do version comparisons even on Conary versions for the purposes of satisfying dependencies fails utility tests. If you say that a shadow does not satisfy a dependency that its parent satisfies, then shadows are almost useless for creating derivative distributions. However, if you say that a shadow does satisfy a dependency that its parent satisfies, then a shadow that intentionally removes some particular capability relative to its parent will falsely satisfy versioned dependencies. Trying to do strict linear comparisons in the Conary version tree universe just does not work.
Conary separates dependencies into different spaces that are provided with individual semantics. Each ELF shared library provides a soname dependency that includes the ABI (for example, SysV), class (ELFCLASS32 or ELFCLASS64 encoded as ELF32 or ELF64, respectively), and instruction set (x86, x86_64, ppc, and so forth) as well as any symbol versions (GLIBC_2.0, GLIBC_2.1, ACL_1.0 and so forth). The elements are stored as separate flags. Programs that link to the shared libraries have a dependency with the same format. These dependencies (requirements or provisions) are coded explicitly as a soname dependency class. The order in which the flags are mentioned is irrelevant.
Trove dependencies are limited to components, since they are the only normal troves that directly reference the files needed to satisfy the dependencies. (Filesets also contain files, but they are always files pulled from troves, so they are not the primary sources of the files, and they are not intended for this use.) By default, a trove dependency is just the name of the trove, but it can also include capability flags, whose names are arbitrary and not interpreted by Conary except checking for equality (just like upstream versions).
This provides the solution to the version comparison problem. Trove A's recipe does not really require upstream version 1.2.3 or greater of trove B:devel in order to build. Instead, it requires some certain functionality in trove B:devel. The solution, therefore, is for package B to provide a relevant capability flag describing the necessary interface, and for trove A's recipe to require trove B:devel with that capability flag. The capability flag could be as simple as 1.2.3, meaning that it supports all the interfaces supported by upstream version 1.2.3 (the meaning of any package's capability flag is relative to only that package). So package B's recipe would have to call r.ComponentProvides('1.2.3') and trove A's recipe would have to require 'B:devel(1.2.3)'.
This solution does require cooperation between the packagers of A and B, but only in respect to a single context. This means that you may choose to shadow trove B in order to add this capability flag in the context of your derived distribution, if your upstream distribution does not provide the capability your package requires.
Do not add trove capability flags without good reason, especially for build requirements. They add complexity that is not always useful. Usually, the development branch for a distribution just needs to be internally consistent, and adding lots of capability flags will just make it harder for someone else to make a derivative work from your distribution, particularly if they are deriving from multiple distributions at once (a reasonable thing to do in the Conary context).
Conary's build requirements are intentionally limited to trove requirements. This facilitates bootstrapping from sources; when troves have not yet been built, the information on which trove provides a certain file name or library soname is simply not available.
In general, there are three main kinds of build requirements: :runtime components (and their dependencies) for programs that need to run at build time, :devel components (and their dependencies) for libraries to which you need to link, and :data components for information in /usr/share which is not associated with a :runtime component. (If there is a :runtime component associated with a :data component, then the :runtime component automatically depends on the :data component.) These three components comprise the vast majority of build requirements.
Build requirements need to be added to a list that is part of recipe metadata. Along with name and version, there is a list called buildRequires, which is simply a list of trove names (including, if necessary, flavors, branch names, and capability flags, but not versions). It can be extended conditionally based on flavors.
buildRequires = [
'a:devel(A_CAPABILITY)',
'gawk:runtime',
'pam:devel[!bootstrap]',
'sqlite:devel=:sqlite2',
]
if Use.gtk:
buildRequires.extend([
'gtk:devel', 'glib:devel'])
The buildRequires list does not have to be exhaustive; you can depend on transitive install-time dependency closure for the troves you list. That is to say, in the example above, you do not have to explicitly list gtk:devellib, because gtk:devel has an install-time requirement for gtk:devellib. (The buildRequires lists do not themselves have transitive closure, as that would be meaningless; you never require a :source component in a buildRequires list, and the dependencies that the other components carry are install-time dependencies.)
Build requirements for :runtime components can be a little bit hard to find if you already have a complete build environment, because some of them are deeply embedded in scripts. It is possible to populate a changeroot environment with only those packages listed in the buildRequires list and their dependencies, then chroot to that environment and build in it and look for failures, but it is not likely to be a very useful exercise. The best approach here is to add items to address known failure cases. Then comment them so that you remember and others can find out why you added them.
Build requirements for :devel components are much simpler. Cook the recipe to a local changeset, and Conary will print out a list of :devel components that you need to add to buildRequires. In Conary 0.61.7, this is a warning; the build will still complete. In some future version not far off, this warning will become an error. This will help provide build-to-build consistency.
Similarly, build requirements for tag description files are also automatically enforced. If a tag description is found to apply, you must either add a build requirement for the trove that contains the tag description file (Conary will tell you which trove or troves to add), or you can call TagSpec manually to tag the files.
The best news about runtime requirements is that you can almost ignore the whole problem. The automatic soname dependencies handle almost everything for you without manual intervention.
There are also some automatic file dependencies, which present a little bit of an asymmetry. Script files automatically require their interpreters. That is, if a file starts with #!/bin/bash that file (and thereby its component) automatically has a requirement for file: /bin/bash added to it. However, there is no automatic provision of file paths. This is because files are not primarily accessed by their paths, but rather by a long numeric identifier (rather like an inode number in a filesystem, but much longer, and random rather than sequential in nature). Files can be tagged as providing their path, but this must be done manually. In practice, this is not a big problem; most programs that normally act as script interpreters are already tagged as providing their paths, and so the exceptions tend to exist within a single trove. Those cases are easy to fix; Conary refuses to install a trove saying that it cannot resolve a file: /usr/bin/foo dependency, but the trove itself contains the /usr/bin/foo file. Just add r.Provides('file', '%(bindir)s/foo') to the recipe.
The hard job with any dependency system is working out the dependencies for shell scripts. It is not practical to make shell dependencies automatic for a variety of reasons (including the fact that shell scripts could generate additional dependencies from the text of their input), and so it remains a manual process. If you are lucky, the package maintainer has listed the requirements explicitly in an INSTALL or README file. If not, you need to glance through shell scripts looking for programs that they call. Since this is not a new problem, you can in practice (for some packages, at least), find the results of other people's efforts in this direction by reading RPM spec files and dpkg debian/rules. This also tends to be an area where dependencies accrete as a result of bug reports.
There is one place where you need to be much more careful about listing the requirements of shell scripts: you must explicitly list all the requirements of the tag handlers you write. This should not be a great burden; most tag handlers are short and call only a few programs. But if you do not list them, Conary cannot ensure that the tag handlers can always be run, which can jeopardize not only successful installation but also rollback reliability.