| RPM-Make-DWIW documentation | Contained in the RPM-Make-DWIW distribution. |
RPM::Make::DWIW - Create an RPM from a hashref
use RPM::Make::DWIW;
my $spec = {
tags => {
Summary => 'ACME DB client',
Name => 'acmedb_client',
Version => '1.3',
Release => '3',
License => 'GPL',
Group => 'Applications/Database',
#Source => 'ftp://ftp.acme.com/acmedb_client-1.3.tar.gz',
#URL => 'http://www.acme.com/acmedb_client/',
#Distribution => 'ACME',
#Vendor => 'ACME Software, Inc.',
#Packager => 'Adam Acme <aa@acme.com>',
},
description => 'Client libraries and binary for ACME DB',
items => [
# first set defaults for following items:
{
defaults => 1,
type => 'file',
mode => '0755',
owner => 'root',
group => 'wheel',
},
{
src => '../src/acme-client',
dest => '/usr/bin/acme-client',
},
{
src => '../src/acme-client.conf',
dest => '/etc/acme-client.conf',
mode => '0644',
},
{
src => '../src/acme.so',
dest => '/usr/lib/libacmeclient.so.1',
mode => '0644',
},
{
type => 'dir',
dest => '/var/log/acme-client/transcripts',
mode => '0777',
},
],
post => 'ldconfig',
postun => 'ldconfig',
};
RPM::Make::DWIW::write_rpm($spec);
This module creates an RPM package from a description hashref.
It has nothing to do with source code or build processes. It assumes that whatever files you want to include in the RPM already exist.
This module can create RPMs for executable files, shared objects, or any other installable file. It is not specialized for installing Perl.
You control the ownership and permissions of each file as installed, independent of the ownership and permissions in the source tree or build directory. You do not have to be root to create the RPM.
Under the covers, this module uses the rpmbuild command to create the RPM. It also creates a temporary dir, which it removes if all went well.
Write an RPM file from the given spec hashref. Spec must contain:
A hashref of metadata specified by RPM. The mandatory tags are Summary, Name, Version, License and Group. The optional tags are Source, URL, Distribution, Vendor and Packager. The meaning of these tags is specified by RPM.
The long description of the package. May contain newlines.
An array of hashrefs, each representing a file or directory to include in the RPM, or a defaults block which sets defaults for subsequent items.
Where you want to copy the file from when building the RPM; typically a relative path into your build directory. Directories have no src.
Location where you want RPM to install the file or create the directory when the RPM is installed.
Access mode that you want the item to have after installation, e.g. '0755' for executables. Must be a string, not a raw octal number.
Unix user/group that you want the item to have after installation.
May be file or dir. If it's file, the item must have src.
If config_p=1, this item is marked as a configuration file. RPM handles them differently.
If defaults=1, this item does not represent a file/dir; it just sets defaults for all items downstream. This way you can avoid repeating mode, user and group when you have several items that share the same settings.
The absence of a key in a defaults block does not remove that key from the current defaults; to do that, set the value to "".
More than one defaults block can occur in the items array. Each defaults block affects items downstream of it. Each defaults block inherits any defaults it does not override.
Spec may also contain:
An array of hashrefs, each having name and optionally min_ver. Example:
requirements => [
{
name => 'libxml2',
min_version => '2.6.0',
}
]
This requires an RPM package called libxml2 or a package offering that capability. The package must have verison 2.6.0 or higher.
Use this to show dependency on other packages. If your package includes executables or shared objects, RPM will examine them with ldd(1) and add dependencies.
Shell commands to run pre/post installation and pre/post uninstallation. May contain newlines. If installing a shared object, generally include 'ldconfig' in both post and postun.
By default this module removes its temp working directory if the RPM builds successfully. If you want to preserve the directory, for instance to examine the specfile, set cleanup = 0.
Get an example spec hashref.
Get the filename of the created RPM. Only call this after creating an RPM.
This module dies on errors. This leaves the temporary build directory intact for inspection. In some cases, you may want to wrap the module in an eval.
rpmbuild(8)
Written by Asher Blum <asher@wildsparx.com> in 2010.
Copyright (C) 2010 Asher Blum. All rights reserved. This code is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
| RPM-Make-DWIW documentation | Contained in the RPM-Make-DWIW distribution. |
# $Header: /usr/local/cvsroot/apb/lib/RPM-Make-DWIW/lib/RPM/Make/DWIW.pm,v 1.4 2010-03-08 06:10:49 asher Exp $ package RPM::Make::DWIW; use strict; no warnings 'uninitialized'; use vars qw( $VERSION ); $VERSION = '0.2'; my $FINAL_RPM_PATH; my $TOP; ## validation: key => type, mandatory my $TOP_VAL = { tags => [ {}, 1 ], description => [ '', 1 ], items => [ [], 1], requirements => [ [], 0], pre => [ '', 0 ], post => [ '', 0 ], preun => [ '', 0 ], postun => [ '', 0 ], cleanup => [ '', 0 ], }; my $TAGS_VAL = { Summary => [ '', 1 ], Name => [ '', 1 ], Version => [ '', 1 ], Release => [ '', 1 ], License => [ '', 1 ], Group => [ '', 1 ], Source => [ '', 0 ], URL => [ '', 0 ], Distribution => [ '', 0 ], Vendor => [ '', 0 ], Packager => [ '', 0 ], }; my $ITEM_VAL = { type => [ '', 1 ], dest => [ '', 1 ], src => [ '', 0 ], mode => [ '', 1 ], owner => [ '', 1 ], group => [ '', 1 ], defaults => [ '', 0 ], config_p => [ '', 0 ], }; ## example spec my $X = { tags => { Summary => 'A CD player app that rocks!', Name => 'cdplayer', Version => '1.2', Release => '3', License => 'GPL', Group => 'Applications/Sound', #Source => 'ftp://ftp.gnomovision.com/pub/cdplayer/cdplayer-1.0.tgz', #URL => 'http://www.gnomovision.com/cdplayer/cdplayer.html', #Distribution => 'WSS Linux', #Vendor => 'White Socks Software, Inc.', #Packager => 'Santa Claus <sclaus@northpole.com>', }, description => 'abc def ghi', items => [ { defaults => 1, owner => 'root', group => 'admin', mode => '0644', }, { src => 'abc.txt', dest => '/usr/bin/abc.txt', mode => '0755', owner => 'cdplayer', group => 'admin', }, { src => 'def.txt', dest => '/usr/lib/def.txt', }, { src => 'def.txt', dest => '/etc/cdplayer.conf', config_p => 1, }, { dest => '/tmp/acme6', type => 'dir', mode => '0777', }, ], requirements => [ { name => 'libxml2', min_version => '2.6.0', } ], post => '/sbin/ldconfig', cleanup => 0, }; ## mkdir or die sub xmkdir { my $dir = shift; mkdir($dir) or die "Can't mkdir $dir: $!"; } sub mk_dirs { $TOP = "topdir-$$"; system("rm -rf $TOP"); # just in case it exists xmkdir($TOP); xmkdir("$TOP/RPMS"); # where the rpm will end up xmkdir("$TOP/BUILD"); # ?? xmkdir("$TOP/root"); # where rpmbuild will take files from } sub rm_dirs { die "top not defined" unless $TOP; system("rm -rf $TOP"); } ## generate RPM spec file as string sub mk_spec { my $x = shift; my $t = scalar localtime; my $res = "## autogenerated by $0 - $t\n\n"; my $tags = $x->{ tags }; foreach my $key(sort keys %$tags) { $res .= "$key: $tags->{ $key }\n"; } $res .= "\n%description\n$x->{ description }\n\n"; foreach my $dep(@{ $x->{ requirements } }) { my $mv = defined $dep->{ min_ver } ? " >= $dep->{ min_ver }" : ''; $res .= "requires: $dep->{ name }$mv\n"; } $res .= "\n%files\n"; my $items = get_items($x); foreach my $item(@$items) { $res .= mk_spec_file_line($item) . "\n"; } foreach my $section(qw( pre post preun postun )) { $res .= "\n\n%$section\n$x->{ $section }\n\n" if $x->{ $section }; } $res; } ## given file (or dir) hashref, return specfile line sub mk_spec_file_line { my $file = shift; foreach my $k(qw( mode owner group dest )) { die "Missing key: $k in item" unless defined $file->{ $k }; } my $line = "%attr($file->{ mode } $file->{ owner } $file->{ group }) $file->{ dest }"; $line = "%config $line" if $file->{ config_p }; $line; } ## given spec hashref, write specfile sub write_spec { my $x = shift; spew("$TOP/specfile", mk_spec($x)); } ## cp src file to dest or die; create dirs as needed sub cpx { my($src, $dest, $mode) = @_; die "Invalid mode '$mode'" unless $mode =~ /^\d{4}$/; die "Not found: $src" unless -e $src; my @parts = split /\//, $dest; pop @parts; my @p2; while(@parts) { push @p2, shift @parts; my $dir = join('/', @p2); unless(-e $dir) { xmkdir($dir); } } system('/bin/cp', $src, $dest) && die "Failed to cp '$src' to '$dest'"; #system('/bin/chmod', $mode, $dest) && die "Failed to chmod '$dest'"; } ## given spec hashref, cp necessary files into tmp tree sub cp_files { my $x = shift; my $files = get_files($x); foreach my $file(@$files) { $file->{ dest } =~ m|^/| or die "Dest path must start with /"; cpx($file->{ src }, "$TOP/root$file->{ dest }", $file->{ mode }); } } ## mk dirs explicitly requested ## wait, is this any good? rpm copy dirs? sub mk_specified_dirs { my $x = shift; my $dirs = get_dirs($x); foreach my $dir(@$dirs) { system("mkdir -p -m $dir->{ mode } $TOP/root$dir->{ dest }") && die "Failed to mkdir '$dir->{ dest }'"; } } #rpmbuild -bb --root `pwd`/root --define "_topdir /space/asher/sand/rpm/cdplayer-example/topdir" specfile2 ## create rpm or die sub xmk_rpm { chomp (my $here = `pwd`); my $rc = system( qq[rpmbuild -bb --root $here/$TOP/root --define "_topdir $here/$TOP" $TOP/specfile > $TOP/rpm.out 2>&1]); if($rc) { print STDERR "Error: see $TOP/rpm.out\n"; exit -1; } } ## given x and RPM, check that RPM has the right files or die sub verify_rpm { my($x, $rpm) = @_; my $items = get_items($x); my $want_files = join(' ', sort map { $_->{ dest } } @$items ); my $cmd = "rpm -q -p --filesbypkg $rpm"; chomp(my @res = `$cmd`); my $have_files = join(' ', sort map { /\S+\s+(\S+)/ } @res) or die "No files found with '$cmd'"; if($want_files ne $have_files) { print STDERR "RPM $rpm does not have expected files:\nWant: $want_files\n\nHave: $have_files\n\n$cmd\n"; exit -1; } } sub get_rpm_path { chomp(my @res = `find $TOP/RPMS -type f`); die "RPM not found" unless @res; ## should never happen die "more than 1 rpm found" if @res > 1; $res[0]; } ## copy the new rpm up to this level or die sub xcp_rpm_here { my $rpm_path = shift; $rpm_path =~ m|([^/]+)$| or die "Invalid rpm_path: '$rpm_path'"; $FINAL_RPM_PATH = $1; system("cp $rpm_path .") && exit -1; } sub spew { my($fn, $page) = @_; open F, ">$fn" or die "Can't open $fn: $!"; print F $page; close F; } ## get files/dirs/all items, excluding defaults blocks sub get_files { my $x = shift; [ grep { $_->{ type } eq 'file' && !$_->{ defaults } } @{ $x->{ items } } ]; } sub get_dirs { my $x = shift; [ grep { $_->{ type } eq 'dir' && !$_->{ defaults } } @{ $x->{ items } } ]; } sub get_items { my $x = shift; [ grep { !$_->{ defaults } } @{ $x->{ items } } ]; } ## return error msg or '' if valid sub validate_hashref { my($val, $x) = @_; my @err; foreach my $key(keys %$x) { if(!$val->{ $key }) { push @err, "Unknown key: $key"; } my $r0 = ref $val->{ $key }[0]; my $r1 = ref $x->{ $key }; if($r0 ne $r1) { push @err, "Wrong type: $key (got '$r1', expected '$r0)"; } } foreach my $key(keys %$val) { if($val->{ $key }[1] && !$x->{ $key }) { ## mand && missing push @err, "Missing key: $key"; } } join('; ', @err); } ## validate or die with msg sub xvalidate_hashref { my($val, $x, $name) = @_; my $err = validate_hashref($val, $x) or return; print STDERR "Error in $name: $err\n"; exit -1; } sub validate_spec { my $spec = shift; xvalidate_hashref($TOP_VAL, $spec, 'top level'); xvalidate_hashref($TAGS_VAL, $spec->{ tags }, 'tags'); my $n = 0; my $items = get_items($spec); foreach my $item(@$items) { xvalidate_hashref($ITEM_VAL, $item, "item $n"); $n ++; } 1; } ## add default vals to any items that lack them ## modifies spec sub apply_defaults { my($x) = @_; my %d = ( type => 'file' ); foreach my $item(@{ $x->{ items } }) { if($item->{ defaults }) { # it is a defaults block; modify our defaults while(my($k, $v) = each %$item) { next if $k eq 'defaults'; $d{ $k } = $v; } } else { # apply defaults to this item while(my($k, $v) = each %d) { $item->{ $k } = $v unless defined $item->{ $k }; } } } } sub apply_global_defaults { my($x) = @_; $x->{ cleanup } = 1 unless exists $x->{ cleanup }; } ## public sub get_rpm_filename { $FINAL_RPM_PATH; } ## public sub get_example_spec { $X; } ## public ## pass me a spec hashref sub write_rpm { my($spec) = shift; apply_global_defaults($spec); apply_defaults($spec); validate_spec($spec); mk_dirs(); write_spec($spec); cp_files($spec); mk_specified_dirs($spec); xmk_rpm(); my $rpm_path = get_rpm_path(); verify_rpm($spec, $rpm_path); xcp_rpm_here($rpm_path); rm_dirs() if $spec->{ cleanup }; 1; } 1; __END__