Chapter 18

Programming RPM with Perl

Copyright (c) 2005 by Eric Foster-Johnson. This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at http://www.opencontent.org/openpub/).

In This Chapter

*	Using the RPM2 module to access information on package files

*	Querying the RPM database from Perl

*	Cross-referencing capabilities by the packages that provide and require capabilities

*	Extracting information on packages

Perl is one of the most popular scripting languages. Used by system administrators, software developers, and a host of other users, Perl runs on many operating systems including Linux, UNIX, and Windows. Perl stands for Practical Extraction and Report Language, or sometimes Pathologically Eclectic Rubbish Lister.

Note

In the same vein, LISP stands for Lots of Irritating Single Parenthesis and COBOL for Completely Obnoxious Business Oriented Language.

I began my book Cross-Platform Perl (John Wiley & Sons, 2000) by mentioning that when I first started learning Perl, I thought it was an evil plot. I still do. But it is a very practical evil plot. You can get a lot of work done with Perl, and quickly.

Because of a long history of text processing, Perl is especially popular among system administrators. Perl also supports add-on packages, called modules. You can find thousands of add-on modules for text processing, networking, and a plethora of other tasks. There are so many modules available that some people who don’t like the Perl syntax script with Perl anyway, because the available modules save a lot of time.

Cross Reference

See search.cpan.org, the Comprehensive Perl Archive Network, for a listing of many Perl modules.

This chapter covers working with RPM files and the RPM database using Perl. You can combine RPM usage with other Perl usage, such as generating HTML files or downloading RPMs over a network link.

Cross Reference

Many of the RPM tools covered in Chapter 8 are written in Perl.

Getting and Using the Perl RPM Modules

A number of Perl RPM modules are available. No one module provides all the features you need, although with time, the Perl modules will consolidate into a few modules that most everyone uses. As of this writing, the RPM2 module, by Chip Turner of Red Hat, provides the most recent approach to working with the RPM system from Perl. This chapter covers the RPM2 module.

Red Hat Linux 8.0 comes with a perl-RPM2 package, which you need to install to use this module. Otherwise, you can download the module from www.cpan.org. Install this module, as well as the perl module, which provides the Perl language interpreter. Once you have this module installed and the perl package installed, you are ready to go.

Note

The version of the perl-RPM2 package that ships with Red Hat Linux 8.0 has a bug in that it will not open package files that were created with the version of rpm that ships with Red Hat Linux 8.0. That is, the Perl module cannot read package files that ship with Red Hat Linux. You can read older package files, though. This problem only affects attempts to read .rpm files, not installed packages. The bug is related to reading signed packages but not having the GPG keys in the keyring. The latest version on search.cpan.org fixes this problem.

The RPM2 module contains Perl methods to work on two types of RPM objects: RPM files and installed packages.

Working with RPM Files

The RPM2 module provides a top-level object, RPM2, that acts as an entry point into the module. From the RPM2 object, you either open the RPM database, covered in the "Programming with the RPM Database" section, or open an RPM package file, covered here.

The first step in working with an RPM file is to open the file inside a Perl script.

Opening package files

The open_package subroutine opens an RPM package file and returns a header object (an RPM2::Header). The basic syntax follows:

my $header = RPM2->open_package( $filename );

For example:

my $header = RPM2->open_package("jikes-1.14-1-glibc-2.2.i386.rpm");

After you’ve opened a package, you can perform a number of query operations on the header object returned by the open_package subroutine.

Listing tags from the package

Each RPM package has information stored under a variety of tags, such as the package name under the NAME tag and the package long description under the DESCRIPTION tag.

Cross Reference

These are the same tags introduced with the --queryformat option to the rpm command discussed in Chapter 5.

The tag subroutine returns the value of a given tag. For example, to get the name of the package, use the NAME tag:

use RPM2;

my $header = RPM2->open_package("jikes-1.14-1-glibc-2.2.i386.rpm" );

print $header->tag("NAME"), "\n";

Pulling this together, Listing 18-1 shows example script that lists the name and one-line short summary of a package file.

Listing 18-1: rpmsum.pl

#!/usr/bin/perl

#

# Lists summary from an RPM package file

# Usage:

# rpmsum.pl package_name.rpm

#

use strict;

use RPM2;

my $header = RPM2->open_package( $ARGV[0] );

print $header->tag("NAME"), ": ", $header->tag("SUMMARY"), "\n";

Enter this script and name the file rpmsum.pl.

When you run this script, you need to pass the name of a package file on the command line. For example:

$ ./rpmsum.pl jikes-1.14-1-glibc-2.2.i386.rpm

jikes: java source to bytecode compiler

Convenience methods

The RPM2 module includes convenience methods for all RPM tags. This means you can use the method name in place of tag("NAME"). For example:

print $header->name(), ": ", $header->summary(), "\n";

Listing the name and version

The RPM2 module provides a handy subroutine for getting the NAME, VERSION, RELEASE, and EPOCH tags, often abbreviated as NVRE. The subroutine, as_nvre, returns a single string with these values in the standard format, with the values separated by minus signs.

Note

Usually, the EPOCH tag has no value. If there is an EPOCH value, you will see it output first, and then a colon, and then the name, version, and release values. For example:

5:redhat-config-httpd-1.0.1-13

In this case, the EPOCH value is 5.

You can call this subroutine on any header object, or any package object to get the full name of the package. For example:

print $header->as_nvre(), "\n";

Checking whether the package is a source package

Another handy subroutine tells you if an RPM file represents a source RPM or a binary RPM. The is_source_package subroutine returns a true value if the package is a source package, and a false value otherwise.

The rpmpkg.pl script, shown in Listing 18-2, shows how to use the as_nvre and is_source_package subroutines.

Listing 18-2: rpmpkg.pl

#!/usr/bin/perl

#

# Queries RPM package file and prints

# out name and whether this is a source pkg.

# Usage:

# rpmpkg.pl package_name

#

use strict;

use RPM2;

my $header = RPM2->open_package( $ARGV[0] );

if ( $header->is_source_package() ) {

print "Source package ", $header->as_nvre(), "\n";

} else {

print $header->as_nvre(), "\n";

}

Programming with the RPM Database

In addition to providing query routines for RPM files, you can also access the RPM database with the RPM2 package.

To access the RPM database, your Perl script must first open the database.

Opening the database

Open the RPM database with a call to open_rpm_db on the RPM2 object. For example:

my $rpm_db = RPM2->open_rpm_db();

You can also specify the directory where the RPM database resides. This is most useful for accessing a database in a non-standard location. For example:

my $rpm_db = RPM2->open_rpm_db( "-path" => "/var/lib/rpm" );

Note

The -path is normally used as a Perl bareword but is shown here as a string.

Once you have an RPM database object, you can call one of the find subroutines to find packages in most of the same ways as supported by the rpm –q command.

Finding packages

The find_by_name subroutine finds a package or packages by name. It returns a Perl list of the entries found. For example, if you installed more than one version of a package, find_by_name would return a list of all the packages at the different versions.

Similar to find_by_name, find_by_name_iter returns an iterator to iterate over the packages that match the query. The iterator approach is usually more efficient.

Iterating over packages

Iterators are important in the RPM2 package because they provide a more efficient interface to potentially large sets of packages, and because iterators more closely match the underlying C API. Furthermore, iterators are very easy to use. Simply call the next subroutine to move ahead to the next element, that is, the next package.

For example:

my $pkg_iter = $rpm_db->find_by_name_iter( "kernel" );

while (my $pkg = $pkg_iter->next() ) {

# Do something ...

}

Listing 18-3 shows a script that acts much like the rpm –q command, without any other command-line options.

Listing 18-3: rpmname.pl

#!/usr/bin/perl

#

# Queries RPM database for given package.

# Usage:

# rpmname.pl package_name

#

use strict;

use RPM2;

my $rpm_db = RPM2->open_rpm_db( "âˆ’path" => "/var/lib/rpm" );

my $pkg_iter = $rpm_db->find_by_name_iter( $ARGV[0] );

while (my $pkg = $pkg_iter->next() ) {

print $pkg->tag("NAME"), "-", $pkg->tag("VERSION"), "\n";

}

$rpm_db->close_rpm_db();

When you run this script, you need to pass the name of a package to query. For example:

$ ./rpmname.pl kernel

kernel-2.4.18

Additional query subroutines

The find_by_name_iter subroutine finds a package by its name. The RPM2 module also supports a number of other query routines, listed in Table 18-1.

Table 18-1 RPM2 module query routines

Routine	Usage
find_all()	Returns a list with all the packages in the database
find_all_iter()	Returns an iterator over all the packages in the database
find_by_file($filename)	Finds all packages that own the given file, returning a list
find_by_file_iter($filename)	Finds all packages that own the given file, returning an iterator
find_by_name($package_name)	Finds all packages with the given name, returning a list
find_by_name_iter($package_name)	Finds all packages with the given name, returning an iterator
find_by_provides($capability)	Finds all packages that provide the given capability, returning a list
find_by_provides_iter($capability)	Finds all packages that provide the given capability, returning an iterator
find_by_requires($capability)	Finds all packages that require the given capability, returning a list
find_by_requires_iter($capability)	Finds all packages that require the given capability, returning an iterator

To verify the find routines, you can try the following script and compare the results with the rpm command. Listing 18-4 shows the script that finds what package provides a capability and also which packages require the capability.

Listing 18-4: rpmprovides.pl

#!/usr/bin/perl

#

# Queries RPM database for given package,

# listing what it provides and what other

# packages require the capability.

#

# Usage:

# rpmprovides.pl package_name

#

use strict;

use RPM2;

my $rpm_db = RPM2->open_rpm_db();

my $pkg_iter = $rpm_db->find_by_provides_iter( $ARGV[0] );

print "Provides: ", $ARGV[0], "\n";

while (my $pkg = $pkg_iter->next() ) {

print "\t", $pkg->as_nvre(), "\n";

}

# Now, what packages require this capability.

my $pkg_iter2 = $rpm_db->find_by_requires_iter( $ARGV[0] );

print "Requires: ", $ARGV[0], "\n";

while (my $pkg2 = $pkg_iter2->next() ) {

print "\t", $pkg2->as_nvre(), "\n";

}

$rpm_db->close_rpm_db();

When you run this script with the name of a capability, you'll see output like the following:

$ ./rpmprovides.pl httpd

Provides: httpd

httpd-2.0.40-8

Requires: httpd

mod_perl-1.99_05-3

5:redhat-config-httpd-1.0.1-13

mod_python-3.0.0-10

1:mod_ssl-2.0.40-8

Note

The 5: in 5:redhat-config-httpd-1.0.1-13 and 1: in 1:mod_ssl-2.0.40-8 represent the EPOCH tag value.

To verify this script, run the rpm -q command to see if you get the same packages listed. For example:

$ rpm -q --whatprovides httpd

httpd-2.0.40-8

$ rpm -q --whatrequires httpd

mod_perl-1.99_05-3

redhat-config-httpd-1.0.1-13

mod_python-3.0.0-10

mod_ssl-2.0.40-8

In both cases, you see the same packages listed. You can use this technique to verify your scripts.

Note

The find_by_provides_iter subroutine requires the name of a package, such as bash. You cannot pass a file name, such as /bin/bash, to get the name of the package that provides this capability (a file, really).

Getting information on packages

The tag, as_nvre, and is_source_package subroutines that worked on header objects read from RPM files, shown previously, also work with package entries returned from the RPM database.

For example, Listing 18-5 shows a script, rpminfo.pl, that prints out descriptive information about a given package.

Listing 18-5: rpminfo.pl

#!/usr/bin/perl

#

# Queries RPM database for given package and prints info.

# Usage:

# rpminfo.pl package_name

#

use strict;

use RPM2;

my $rpm_db = RPM2->open_rpm_db( "-path" => "/var/lib/rpm" );

my $pkg_iter = $rpm_db->find_by_name_iter( $ARGV[0] );

while (my $pkg = $pkg_iter->next() ) {

printInfo( $pkg );

}

$rpm_db->close_rpm_db();

# Prints info on one package.

sub printInfo {

my($pkg) = shift;

print $pkg->as_nvre(), ", ", $pkg->tag("ARCH"), ", ",

$pkg->tag("OS"), ", ", $pkg->tag("PLATFORM"), "\n";

print $pkg->tag("SUMMARY"), "\n";

print "Group: ", $pkg->tag("GROUP"), "\n";

print $pkg->tag("DESCRIPTION"), "\n";

print "Vendor: ", $pkg->tag("VENDOR"), ", ", $pkg->tag("URL"), "\n";

print "Size: ", $pkg->tag("SIZE"), "\n";

}

When you run this script, you’ll see output like the following:

$ ./rpminfo.pl XFree86

XFree86-4.2.0-72, i386, linux, i386-redhat-linux-gnu

The basic fonts, programs and docs for an X workstation.

Group: User Interface/X

XFree86 is an open source implementation of the X Window System. It

provides the basic low level functionality which full fledged

graphical user interfaces (GUIs) such as GNOME and KDE are designed

upon.

Vendor: Red Hat, Inc., http://www.xfree86.org

Size: 30552239

Listing the Installed Date

The installed date is a number value representing the number of seconds since the start of the UNIX epoch, January 1, 1970, which predates the start of the Linux epoch by about 20 years. So, when you get the value of the INSTALLTIME tag, you’ll see a meaningless number.

To make sense of this number, pass the value to the Perl localtime function. Listing 18-6 shows an example of this.

Listing 18-6: rpmdate.pl

#!/usr/bin/perl

#

# Queries RPM database for given package,

# prints out name, vendor, and date installed.

# Usage:

# rpmdate.pl package_name

#

use strict;

use RPM2;

my $rpm_db = RPM2->open_rpm_db();

my $pkg_iter = $rpm_db->find_by_name_iter( $ARGV[0] );

while (my $pkg = $pkg_iter->next() ) {

printDate( $pkg );

}

$rpm_db->close_rpm_db();

# Prints installation data for one package.

sub printDate {

my($pkg) = shift;

my $date = localtime( $pkg->tag("INSTALLTIME") );

printf("%-20s %-17s %s\n", $pkg->as_nvre(), $pkg->tag("VENDOR"), $date);

}

Note

The printf function in this script can do something the rpm command cannot do. Even with the --queryformat option, you cannot group multiple items and then set the size; with Perl, you can. Simply assign the multiple values to a string, or use the handy as_nvre subroutine, which gathers up to four tags together into one string.

When you pass the name of a package to this script, you’ll see the date the package was installed. For example:

$ ./rpmdate.pl kernel

kernel-2.4.18-14 Red Hat, Inc. Sat Oct 5 12:29:58 2002

Handling String Array Tags

Not only is the date stored in a format that adds complication to your script. A number of tags are string arrays, not scalar strings. This means you may see output that is all mashed together.

To help deal with this, the following subroutine takes in an array of strings and returns a string that is built using a passed-in delimiter:

sub arrayToString {

my($sep) = shift;

my(@array) = @_;

my($str);

$str = $array[0];

for ( $i = 1; $i < $#array; $i++ )

{

$str = $str . $sep . $array[$i];

}

return $str;

}

Note

Show your Perl expertise and earn extra points by implementing the arrayToString subroutine as a single Perl statement that uses the join function.

The following list shows the tags that are an array of strings:

*	BASENAMES

*	CHANGELOGNAME

*	CHANGELOGTEXT

*	DIRNAMES

*	FILEGROUPNAME

*	FILELANGS

*	FILELINKTOS

*	FILEMD5S

*	FILEUSERNAME

*	OLDFILENAMES

*	PROVIDENAME

*	PROVIDEVERSION

*	REQUIRENAME

*	REQUIREVERSION

Cross Reference

Chapter 5 covers more on these tags.

Listing the Files In A Package

The files subroutine provides a list of all the files in a package. Listing 18-7 shows how to access this list.

Listing 18-7: rpmfiles.pl

#!/usr/bin/perl

#

# Queries RPM database for given package,

# prints out the files in the package.

# Usage:

# rpmfiles.pl package_name

#

use strict;

use RPM2;

my $rpm_db = RPM2->open_rpm_db();

my $pkg_iter = $rpm_db->find_by_name_iter( $ARGV[0] );

while (my $pkg = $pkg_iter->next() ) {

printFiles( $pkg );

}

$rpm_db->close_rpm_db();

# Prints installation data for one package.

sub printFiles {

my($pkg) = shift;

my $files = arrayToString("\n", $pkg->files() );

print "Files:\n", $files, "\n";

}

sub arrayToString {

my($sep) = shift;

my(@array) = @_;

my($str);

$str = $array[0];

for ( my $i = 1; $i < $#array; $i++ )

{

$str = $str . $sep . $array[$i];

}

return $str;

}

When you run this script, you’ll see output like the following:

$ ./rpmfiles.pl jikes

Files:

/usr/bin/jikes

/usr/doc/jikes-1.17/license.htm

Comparing versions

The RPM2 module overrides the spaceship operator, <=>, to perform version comparisons between packages. The script in Listing 18-8 shows how to compare all local RPM files against the newest installed version of the same package, if the package is installed.

Listing 18-8: rpmver.pl

#!/usr/bin/perl -w

#

# Compare versions of all *.rpm files against the

# latest packages installed (if installed)

#

# Usage:

# rpmver.pl

# This script looks for all *.rpm files.

#

use strict;

use RPM2;

my $rpm_db = RPM2->open_rpm_db();

for my $filename (<*.rpm>) {

my $h = RPM2->open_package( $filename );

# Ensure we compare against the newest

# package of the given name.

my ($installed) =

sort { $b <=> $a } $rpm_db->find_by_name($h->name);

if (not $installed) {

printf "Package %s not installed.\n", $h->as_nvre;

} else {

my ($result) = ($h <=> $installed);

if ($result < 0) {

printf "Installed package %s newer than file %s\n",

$installed->as_nvre,

$h->as_nvre;

} else {

printf "File %s newer than installed package %s\n",

$h->as_nvre,

$installed->as_nvre;

}

}

}

The sort { $a <=> $b } in front of the find_by_name call sorts all the packages of that name by the version number, so that the comparison is performed against the newest installed version of the package. The ($h <=> $installed) compares the header from the RPM file on disk against the newest installed version of the package.

When you run this script, you’ll see output like the following, depending on which RPM files you have in the local directory:

$ perl rpmver.pl

Package acroread-4.0-0 not installed.

Package canvas-7.0b2.0-1 not installed.

Installed package jikes-1.18-1 newer than file jikes-1.14-1

Installed package SDL-1.2.4-5 newer than file SDL-0.9.9-4

Package ted-2.8-1 not installed.

Closing the database

When you are done with the RPM database, call close_rpm_db, as shown following:

$rpm_db->close_rpm_db();

Note that this call is not necessary, as the RPM2 module will close the database when the object, in this case $rpm_db, goes out of scope.

Where to Go from Here

One of the strengths of Perl is that there are so many add-on packages available. In addition, Perl is really strong in text processing. You can combine these strengths to provide cleaner output for RPM database queries, for example, avoiding the complex syntax for the --queryformat option to the rpm command. Perl can do more than the --queryformat option allows. For example, you can combine multiple values together into a Perl string and then format the output. The --queryformat option only allows formatting on each value individually, not groups of values.

In addition, you can combine one of the Perl templating modules, such as Text::Template or HTML::Template, to create an HTML page for a given package. You could use Perl to create formatted HTML pages for all the installed packages on your system, with HTML links to cross-reference all the dependencies.

Cross Reference

Download these modules from the CPAN site, www.cpan.org.

This chapter covers the RPM2 module. Right now, the RPM2 module supports only querying packages and the RPM database. Future versions will likely add the ability to install, update, and remove packages.

In addition to this module, you can find an RPM module with RPM::Header and RPM::Database classes. Another module, RPM::Specfile, provides the ability to turn Perl modules, such as those stored on CPAN, into RPM packages. The RPM::Specfile module helps create an RPM spec file for a Perl module.

The Perl-RPM-Perlonly bundle provides an alternative version of the RPM::Header module written entirely in Perl with no usage of the C rpm library. This makes RPM access much easier on platforms for which you don’t have the RPM system.

The RPM-Tools bundle includes RPM::Update, which compares the packages installed on your system (listed by calling rpm –qa) with the packages available on another system, that may be available only with a network link. This module can also update packages that are older than the designated master system. RPM::Make, also part of the RPM-Tools bundle, helps create RPM packages from a Perl script. This module does not support all the spec file options described in Chapter 10, but it can help you make simple packages.

You can download all these modules from the CPAN site.

Summary

This chapter introduces the RPM2 add-on module to allow Perl scripts to access information on RPM package files and in the RPM database. To access an RPM file and query information about that file, you need to call the open_package subroutine. Once you’ve opened the file, you can call the tag, as_nvre, is_source_package, and files subroutines on the header object to query data about the package.

To access the RPM database, call open_rpm_db. Once you’ve opened the database, you can call one of the find subroutines, such as find_by_name or find_by_name_iter, to search for packages. The subroutines that have names ending with _iter, such as find_by_name_iter, return an iterator object to iterate over the packages found. The other find subroutines, such as find_by_name, return a Perl list of the packages found.

You can then call the tag, as_nvre, and files subroutines on the package objects to query information about the packages.

When you are done with the RPM database, call close_rpm_db.

Chapter 18

Chris Ricker

2003-01-25T20:03:00

Eric Foster-Johnson