#!/usr/bin/perl -w # # # makerpm.pl - A Perl script for building binary distributions # of Perl packages # # This script is Copyright (C) 1999 Jochen Wiedmann # Am Eisteich 9 # 72555 Metzingen # Germany # # E-Mail: joe@ispsoft.de # # You may distribute under the terms of either the GNU General # Public License or the Artistic License, as specified in the # Perl README. # use strict; use Cwd (); use File::Find (); use File::Path (); use File::Spec (); use Getopt::Long (); use vars qw($VERSION); $VERSION = "makerpm 0.1003 22-July-1999, (C) 1999 Jochen Wiedmann"; =pod =head1 NAME makerpm - Build binary distributions of Perl packages =head1 SYNOPSIS Create a SPECS file: makerpm --specs --source=-.tar.gz Apply the SPECS file (which in turn uses makerpm.pl): rpm -ba -.spec =head1 DESCRIPTION The I script is designed for creating binary distributions of Perl modules, for example RPM packages (Linux) or PPM files (Windows, running ActivePerl). =head2 Creating RPM packages To create a new binary and source RPM, you typically store the tar.gz file in F (F in case of SuSE and F in case of Caldera) and do a makerpm --specs --source=-.tar.gz This will create a SPECS file in F (F in case of SuSE and F in case of Caldera) which you can use with rpm -ba /usr/src/redhat/SPECS/-.spec If the default behaviour is fine for you, that will do. Otherwise see the list of options below. =head2 Creating PPM packages Not yet implemented =head2 Command Line Options Possible command line options are: =over 8 =item --build Compile the sources, typically by running perl Makefile.PL make =item --build-root= Installation of the Perl package occurs into a separate directory, the build root directory. For example, a package DBI 1.07 could be installed into F. Binaries are installed into F<$build_root/usr/bin> rather than F, man pages in F<$build_root/usr/man> and so on. The idea is making the build process really reproducible and building the package without destructing an existing installation. You don't need to supply a build root directory, a default name is choosen. =item --copyright= Set the packages copyright message. The default is GNU General Public or Artistic License, as specified in the Perl README =item --debug Turns on debugging mode. Debugging mode prevents most things from really being done and implies verbose mode. =item --help Print the usage message and exit. =item --install Install the sources, typically by running make install Installation doesn't occur into the final destination. Instead a so-called buildroot directory (for example F) is created and installation is adapted relative to that directory. See the I<--build-root> option for details. =item --make= Set path of the I binary; defaults to the location read from Perl's Config module. L. =item --makeopts= Set options for running "make" and "make install"; defaults to none. =item --makemakeropts= If you need certain options for running "perl Makefile.PL", this is your friend. By default no options are set. =item --mode= Set build mode, for example RPM or PPM. By default the build mode is read from the script name: If you invoke it as I, then RPM mode is choosen. When running as I, then PPM mode is enabled. =item --package-name= =item --package-version= Set the package name and version. These options are required for --build and --install. =item --prep Extract the sources and prepare the source directory. =item --rpm-top-dir= =item --rpm-build-dir= =item --rpm-source-dir= =item --rpm-specs-dir= Sets certain directory names related to RPM mode, defaults to F (or F on SuSE Linux or F on Caldera) F<$topdir/BUILD>, F<$topdir/SOURCES> and F<$topdir/SPECS>. =item --rpm-group= Sets the RPM group; defaults to Development/Languages/Perl. =item --setup-dir= Name of setup directory; defaults to -. The setup directory is the name of the directory that is created by extracting the sources. Example: DBI-1.07. =item --source= Source file name; used to determine defaults for --package-name and --package-version. This option is required for --specs and --prep. =item --summary= Summary line; defaults to "The Perl package ". =item --verbose Turn on verbose mode. Lots of debugging messages are emitted. =item --version Print version string and exit. =back =cut package Distribution; $Distribution::TMP_DIR = '/tmp'; foreach my $dir (qw(/var/tmp /tmp C:/Windows/temp D:/Windows/temp)) { if (-d $dir) { $Distribution::TMP_DIR = $dir; last; } } $Distribution::COPYRIGHT = "Artistic or GNU General Public License," . " as specified by the Perl README"; sub new { my $proto = shift; my $self = { @_ }; bless($self, ref($proto) || $proto); if ($self->{'source'} && $self->{'source'} =~ /(.*(?:\/|\\))?(.*)-(.+) (\.(tar\.gz|tgz|zip))$/x) { $self->{'package-name'} ||= $2; $self->{'package-version'} ||= $3; } $self->{'name'} = $self->{'package-name'} or die "Missing package name"; $self->{'version'} = $self->{'package-version'} or die "Missing package version"; $self->{'source_dirs'} ||= [ File::Spec->curdir() ]; $self->{'default_setup_dir'} = "$self->{'name'}-$self->{'version'}"; $self->{'setup-dir'} ||= $self->{'default_setup_dir'}; $self->{'build_dir'} = File::Spec->curdir(); $self->{'make'} ||= $Config::Config{'make'}; $self->{'build-root'} ||= File::Spec->catdir($Distribution::TMP_DIR, $self->{'setup-dir'}); $self->{'copyright'} ||= $Distribution::COPYRIGHT; $self->{'summary'} ||= "The Perl package $self->{'name'}"; $self->{'start_perl'} = $self->{'perl-path'} || substr($Config::Config{'startperl'}, 2); $self->{'start_perl'} = undef if $self->{'start_perl'} eq 'undef'; $self; } sub Extract { my $self = shift; my $dir = shift || File::Spec->curdir(); print "Changing directory to $dir\n" if $self->{'verbose'}; chdir $dir || die "Failed to chdir to $dir: $!"; # Look for the source file my $source = $self->{'source'} || die "Missing source definition"; if (! -f $source) { foreach my $dir (@{$self->{'source_dirs'}}) { print "Looking for $source in $dir\n" if $self->{'debug'}; my $s = File::Spec->catfile($dir, $source); if (-f $s) { print "Found $source in $dir\n" if $self->{'debug'}; $source = $s; last; } } } $dir = $self->{'setup-dir'}; if (-d $dir) { print "Removing directory $dir" if $self->{'verbose'}; File::Path::rmtree($dir, 0, 0) unless $self->{'debug'}; } print "Extracting $source\n" if $self->{'verbose'}; eval { require Archive::Tar; require Compress::Zlib; }; if ($@) { # Archive::Tar is not available; fallback to tar and gzip my $command = "gzip -cd $source | tar xf -"; my $output = `$command 2>&1`; die "Archive::Tar and Compress::Zlib are not available\n" . " and using tar and gzip failed.\n" . " Command was: $command\n" . " Output was: $output\n" if $output; } else { die "Failed to extract archive $source: " . Archive::Tar->error() unless defined(Archive::Tar->extract_archive($source)); } } sub Modes { my $self = shift; my $dir = shift || File::Spec->curdir(); print "Changing directory to $dir\n" if $self->{'verbose'}; chdir $dir || die "Failed to chdir to $dir: $!"; my $handler = sub { my($dev, $ino, $mode, $nlink, $uid, $gid) = stat; my $new_mode = 0444; $new_mode |= 0200 if $mode & 0200; $new_mode |= 0111 if $mode & 0100; chmod $new_mode, $_ or die "Failed to change mode of $File::Find::name: $!"; chown 0, 0, $_ or die "Failed to change ownership of $File::Find::name: $!"; }; $dir = File::Spec->curdir(); print "Changing modes in $dir\n" if $self->{'verbose'}; File::Find::find($handler, $dir); } sub Prep { my $self = shift; my $old_dir = Cwd::cwd(); eval { my $dir = $self->{'build_dir'}; print "Changing directory to $dir\n" if $self->{'verbose'}; chdir $dir || die "Failed to chdir to $dir: $!"; if (-d $self->{'setup-dir'}) { print "Removing directory: $self->{'setup-dir'}\n" if $self->{'verbose'}; File::Path::rmtree($self->{'setup-dir'}, 0, 0); } $self->Extract(); $self->Modes($self->{'setup-dir'}); }; my $status = $@; print "Changing directory to $old_dir\n" if $self->{'verbose'}; chdir $old_dir; die $@ if $status; } sub PerlMakefilePL { my $self = shift; my $dir = shift || File::Spec->curdir(); print "Changing directory to $dir\n" if $self->{'verbose'}; chdir $dir || die "Failed to chdir to $dir: $!"; my $command = "$^X Makefile.PL " . ($self->{'makemakeropts'} || ''); print "Creating Makefile: $command\n" if $self->{'verbose'}; exit 1 if system $command; } sub Make { my $self = shift; if (my $dir = shift) { print "Changing directory to $dir\n" if $self->{'verbose'}; chdir $dir || die "Failed to chdir to $dir: $!"; } my $command = "$self->{'make'} " . ($self->{'makeopts'} || ''); print "Running Make: $command\n"; exit 1 if system $command; if ($self->{'runtests'}) { $command .= " test"; print "Running Make Test: $command\n"; exit 1 if system $command; } } sub ReadLocations { my %vars; my $fh = Symbol::gensym(); open($fh, ") { # Skip comments and/or empty lines next if $line =~ /^\s*\#/ or $line =~ /^\s*$/; if ($line =~ /^\s*(\w+)\s*\=\s*(.*)\s*$/) { # Variable definition my $var = $1; my $val = $2; $val =~ s/\$(\w)/defined($vars{$1})?$vars{$1}:''/gse; $val =~ s/\$\((\w+)\)/defined($vars{$1})?$vars{$1}:''/gse; $val =~ s/\$\{(\w+)\}/defined($vars{$1})?$vars{$1}:''/gse; $vars{$var} = $val; } } \%vars; } sub AdjustPaths { my $self = shift; my $build_root = shift; my $adjustPathsSub = sub { my $f = $_; return unless -f $f && ! -z _; my $fh = Symbol::gensym(); open($fh, "+<$f") or die "Failed to open $File::Find::name: $!"; local $/ = undef; my $contents; die "Failed to read $File::Find::name: $!" unless defined($contents = <$fh>); my $modified; if ($self->{'start_perl'}) { $contents =~ s/^\#\!(\S+)/\#\!$self->{'start_perl'}/s; $modified = 1; } if ($contents =~ s/\Q$build_root\E//gs) { $modified = 1; } if ($modified) { seek($fh, 0, 0) or die "Failed to seek in $File::Find::name: $!"; (print $fh $contents) or die "Failed to write $File::Find::name: $!"; truncate $fh, length($contents) or die "Failed to truncate $File::Find::name: $!"; } close($fh) or die "Failed to close $File::Find::name: $!"; }; File::Find::find($adjustPathsSub, $self->{'build-root'}); } sub MakeInstall { my $self = shift; if (my $dir = shift) { print "Changing directory to $dir\n" if $self->{'verbose'}; chdir $dir || die "Failed to chdir to $dir: $!"; } my $locations = ReadLocations(); my $command = "$self->{'make'} " . ($self->{'makeopts'} || '') . " install"; foreach my $key (qw(INSTALLPRIVLIB INSTALLARCHLIB INSTALLSITELIB INSTALLSITEARCH INSTALLBIN INSTALLSCRIPT INSTALLMAN1DIR INSTALLMAN3DIR)) { my $d = File::Spec->canonpath(File::Spec->catdir($self->{'build-root'}, $locations->{$key})); $command .= " $key=$d"; } print "Running Make Install: $command\n" if $self->{'verbose'}; exit 1 if !$self->{'debug'} and system $command; print "Adjusting Paths in $self->{'build-root'}\n"; $self->AdjustPaths($self->{'build-root'}); my($files, $dirs) = $self->Files($self->{'build-root'}); my $fileList = ''; foreach my $dir (sort keys %$dirs) { next if $dirs->{$dir}; $fileList .= "%dir $dir\n"; } foreach my $file (sort keys %$files) { $fileList .= "$file\n"; } my($filelist_path, $specs_path) = $self->FileListPath(); if ($filelist_path) { my $fh = Symbol::gensym(); (open($fh, ">$filelist_path") and (print $fh $fileList) and close($fh)) or die "Failed to create list of files in $filelist_path: $!"; } $specs_path; } sub Build { my $self = shift; my $old_dir = Cwd::cwd(); eval { my $dir = $self->{'build_dir'}; print "Changing directory to $dir\n" if $self->{'verbose'}; chdir $dir || die "Failed to chdir to $dir: $!"; $self->PerlMakefilePL($self->{'setup-dir'}); $self->Make(); }; my $status = $@; chdir $old_dir; die $@ if $status; } sub CleanBuildRoot { my $self = shift; my $dir = shift || die "Missing directory name"; print "Cleaning build root $dir\n" if $self->{'verbose'}; File::Path::rmtree($dir, 0, 0) unless $self->{'debug'}; } sub Install { my $self = shift; my $old_dir = Cwd::cwd(); my $filelist; eval { my $dir = $self->{'build_dir'}; print "Changing directory to $dir\n" if $self->{'verbose'}; chdir $dir || die "Failed to chdir to $dir: $!"; $self->CleanBuildRoot($self->{'build-root'}); $filelist = $self->MakeInstall($self->{'setup-dir'}); }; my $status = $@; chdir $old_dir; die $@ if $status; $filelist; } package Distribution::RPM; @Distribution::RPM::ISA = qw(Distribution); { my($source_dir, $build_dir, $specs_dir); sub Init { my $proto = shift; my $fatal = shift; my $topdir; if (!$source_dir) { my $rpm_output = `rpm --showrc`; my $rpm_version = `rpm --version`; if ($rpm_version =~ /rpm\s+version\s+2\.+/i) { foreach my $ref (['topdir', \$topdir], ['specdir', \$specs_dir], ['sourcedir', \$source_dir], ['builddir', \$build_dir]) { my $var = $ref->[0]; if ($rpm_output =~ /^$var\s+\S+\s+(.*)/m) { ${$ref->[1]} = $1; } } } elsif ($rpm_version =~ /rpm\s+version\s+3\.+/i) { my $varfunc; $varfunc = sub { my $var = shift; my $val; if ($rpm_output =~ /^\S+\s+$var\s+(.*)/m) { $val = $1; while ($val =~ /\%\{(\S+)\}/) { my $vr = $1; my $vl = &$varfunc($vr); if (defined($vl)) { $val =~ s/^\%\{\Q$vr\E\}/$vl/gs; } else { return undef; } } return $val; } return undef; }; foreach my $ref (['_topdir', \$topdir], ['_specdir', \$specs_dir], ['_sourcedir', \$source_dir], ['_builddir', \$build_dir]) { ${$ref->[1]} = &$varfunc($ref->[0]); } } die "Cannot handle your RPM version: " . ($rpm_version || "") if (!$source_dir or !$specs_dir or !$build_dir) and $fatal; } if (!$topdir) { foreach my $dir ("redhat", "packages", "OpenLinux") { if (-d "/usr/src/$dir") { $topdir = "/usr/src/$dir"; last; } } die "Unable to determine RPM topdir"; } $source_dir ||= "$topdir/SOURCES"; $specs_dir ||= "$topdir/SPECS"; $build_dir ||= "$topdir/BUILD"; return ($source_dir, $build_dir, $specs_dir); } } sub new { my $proto = shift; my $self = $proto->SUPER::new(@_); ($self->{'rpm-source-dir'}, $self->{'rpm-build-dir'}, $self->{'rpm-specs-dir'}) = $proto->Init(1); $self->{'rpm-group'} ||= 'Development/Languages/Perl'; push(@{$self->{'source_dirs'}}, $self->{'rpm-source-dir'}); $self->{'build_dir'} = $self->{'rpm-build-dir'}; $self; } sub Files { my $self = shift; my $buildRoot = shift; my(%files, %dirs); my $findSub = sub { if (-d $_) { $dirs{$File::Find::name} ||= 0; $dirs{$File::Find::dir} = 1; } elsif (-f _) { $files{$File::Find::name} = 1; $dirs{$File::Find::dir} = 1; } else { die "Unknown file type: $File::Find::name"; } }; File::Find::find($findSub, $buildRoot); # Remove the trailing buildRoot my(%f, %d); while (my($key, $val) = each %files) { $key =~ s/^\Q$buildRoot\E//; $f{$key} = $val } while (my($key, $val) = each %dirs) { $key =~ s/^\Q$buildRoot\E//; $d{$key} = $val } (\%f, \%d, \%files, \%dirs); } sub FileListPath { my $self = shift; my $fl = $self->{'setup-dir'} . ".rpmfilelist"; ($fl, File::Spec->catdir($self->{'setup-dir'}, $fl)); } sub Specs { my $self = shift; my $old_dir = Cwd::cwd(); eval { $self->Prep(); $self->Build(); my $filelist = $self->Install(); my($files, $dirs) = $self->Files($self->{'build-root'}); my $specs = <<"EOF"; %define packagename $self->{'name'} %define packageversion $self->{'version'} %define release 1 EOF my $mo = $self->{'makeopts'} || ''; $mo =~ s/\n\t/ /sg; $specs .= sprintf("%%define makeopts \"%s\"\n", ($mo ? sprintf("--makeopts=%s", quotemeta($mo)) : "")); my $mmo = $self->{'makemakeropts'} || ''; $mmo =~ s/\n\t/ /sg; $specs .= sprintf("%%define makemakeropts \"%s\"\n", ($mmo ? sprintf("--makemakeropts=%s", quotemeta($mmo)) : "")); my $setup_dir = $self->{'setup-dir'} eq $self->{'default_setup_dir'} ? "" : " --setup-dir=$self->{'setup-dir'}"; my $makerpm_path = File::Spec->catdir('$RPM_SOURCE_DIR', 'makerpm.pl'); $makerpm_path = File::Spec->canonpath($makerpm_path) . $setup_dir . " --source=$self->{'source'}"; $specs .= <<"EOF"; Name: perl-%{packagename} Version: %{packageversion} Release: %{release} Group: $self->{'rpm-group'} Source: $self->{'source'} Source1: makerpm.pl Copyright: $self->{'copyright'} BuildRoot: $self->{'build-root'} Provides: perl-%{packagename} Summary: $self->{'summary'} EOF if (my $req = $self->{'require'}) { $specs .= "Requires: " . join(" ", @$req) . "\n"; } my $runtests = $self->{'runtests'} ? " --runtests" : ""; $specs .= <<"EOF"; %description $self->{'summary'} %prep $makerpm_path --prep %build $makerpm_path --build$runtests %{makeopts} %{makemakeropts} %install rm -rf \$RPM_BUILD_ROOT $makerpm_path --install %{makeopts} %clean rm -rf \$RPM_BUILD_ROOT %files -f $filelist EOF my $specs_name = "$self->{'name'}-$self->{'version'}.spec"; my $specs_file = File::Spec->catfile($self->{'rpm-specs-dir'}, $specs_name); $specs_file = File::Spec->canonpath($specs_file); print "Creating SPECS file $specs_file\n"; print $specs if $self->{'verbose'}; unless ($self->{'debug'}) { my $fh = Symbol::gensym(); open(FILE, ">$specs_file") or die "Failed to open $specs_file: $!"; (print FILE $specs) or die "Failed to write to $specs_file: $!"; close(FILE) or die "Failed to close $specs_file: $!"; } }; my $status = $@; chdir $old_dir; die $@ if $status; } package main; sub Mode { return "RPM" if $0 =~ /rpm$/i; undef; } sub Usage { my $mode = Mode() || "undef"; my $build_root = File::Spec->catdir($Distribution::TMP_DIR, "-"); my $start_perl = substr($Config::Config{'startperl'}, 2); my ($rpm_source_dir, $rpm_build_dir, $rpm_specs_dir) = Distribution::RPM->Init(1); print < [options] Possible actions are: --prep Prepare the source directory --build Compile the sources --install Install the compiled sources into the buildroot directory --specs Create a SPECS file by performing the above steps in order to determine the list of installed files. Possible options are: --build-root= Set build-root directory for installation; defaults to $build_root. --copyright= Set copyright message, defaults to "GNU General Public License or Artistic License, as specified in the Perl README". --debug Turn on debugging mode --help Print this message --make= Set "make" path; defaults to $Config::Config{'make'} --makemakeropts= Set options for running "perl Makefile.PL"; defaults to none. --makeopts= Set options for running "make" and "make install"; defaults to none. --mode= Set build mode, defaults to $mode. --package-name= Set package name. --package-version= Set package version. --perl-path= Perl path to verify in generated scripts; defaults to $start_perl --require= Set prerequisite packages. May be used multiple times. --runtests By default no "make test" is done. You can override this with --runtests. --setup-dir= Name of setup directory; defaults to - --source= Source file name; used to determine defaults for and . --summary= One line desription of the package; defaults to "The Perl package ". --verbose Turn on verbose mode. --version Print version string and exit. Options for RPM mode are: --rpm-build-dir= RPM build directory; defaults to $rpm_build_dir. --rpm-group= RPM group, default Development/Languages/Perl. --rpm-source-dir= RPM source directory; defaults to $rpm_source_dir. --rpm-specs-dir= RPM specs directory; defaults to $rpm_specs_dir. $VERSION EOF exit 1; } { my %o; Getopt::Long::GetOptions(\%o, 'build', 'build-root=s', 'copyright=s', 'debug', 'help', 'install', 'make=s', 'makemakeropts=s', 'makeopts=s', 'mode=s', 'package-name=s', 'package-version=s', 'prep', 'require=s@', 'rpm-base-dir=s', 'rpm-build-dir=s', 'rpm-source-dir=s', 'rpm-specs-dir=s', 'rpm-group=s', 'runtests', 'setup-dir=s', 'source=s', 'specs', 'summary=s', 'verbose', 'version=s'); Usage() if $o{'help'}; if ($o{'version'}) { print "$VERSION\n"; exit 1} $o{'verbose'} = 1 if $o{'debug'}; my $class = 'Distribution::RPM'; if ($o{'mode'}) { if ($o{'mode'} ne 'rpm') { die "Unknown mode: $o{'mode'}"; } } my $self = $class->new(%o); if ($o{'prep'}) { $self->Prep(); } elsif ($o{'build'}) { $self->Build(); } elsif ($o{'install'}) { $self->Install(); } elsif ($o{'specs'}) { $self->Specs(); } else { die "Missing action"; } } __END__ =pod =head1 INSTALLATION Before using this script, you need to install the required packages: C If you are using Perl 5.00502 or later, then this package is already part of your Perl installation. It is recommended to use the C C packages, if possible. All of these packages are available on any CPAN mirror, for example ftp://ftp.funet.fi/pub/languages/perl/CPAN/modules/by-module To install a package, fetch the corresponding distribution file, for example Archive/Archive-Tar-0.21.tar.gz extract it with gzip -cd Archive-Tar-0.21.tar.gz and install it with cd Archive-Tar-0.21 perl Makefile.PL make make test make install Alternatively you might try automatic installation via the CPAN module: cpan (until Perl 5.00503 you need: perl -MCPAN -e shell) install Archive::Tar install Compress::Zlib install File::Spec (only with Perl 5.004 or lower) =head1 AUTHOR AND COPYRIGHT This script is Copyright (C) 1999 Jochen Wiedmann Am Eisteich 9 72555 Metzingen Germany E-Mail: joe@ispsoft.de You may distribute under the terms of either the GNU General Public License or the Artistic License, as specified in the Perl README. =head1 CPAN This file is available as a CPAN script. The following subsections are for CPAN's automatic link generation and not for humans. You can safely ignore them. =head2 SCRIPT CATEGORIES UNIX/System_administration =head2 README This script can be used to build RPM or PPM packages automatically. =head2 PREREQUISITES This script requires the C package. =head1 TODO =over 8 =item - Handling of prerequisites by reading PREREQ_PM from the Makefile =item - Support for installation and deinstallation scripts =item - Make package relocatable =item - Support for %description. =back =head1 CHANGES 1999-12-10 Peter J. Braam * Fixed the $base_dir: correct naming is topdir and compute it from the rpm --showrc like the rest 1999-09-13 Jochen Wiedmann * Modes: Fixed the use of ||= instead of |=; thanks to Tim Potter, Tim Potter * Now using %files -f 1999-07-22 Jochen Wiedmann * Now falling back to use of "tar" and "gzip", if Archive::Tar and Compress::Zlib are not available. * Added --runtests, suggested by Seth Chaiklin . 1999-07-09 Jochen Wiedmann * Now using 'rpm --showrc' to determine RPM's base dirs. 1999-07-01 Jochen Wiedmann * /usr/src/redhat was used rather than $Distribution::RPM::BASE_DIR. * The AdjustPaths function is now handling files zero size files properly. * An INSTALLATION section was added to the docs that describes the installation of prerequisites. * A warning for being possibly "0" is now suppressed with Perl 5.004. 1999-05-24 Jochen Wiedmann * Added --perl-path and support for fixing startperl in scripts. Some authors don't know how to fix it. :-( =head1 SEE ALSO L, L, L =cut