#!/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.1002 09-June-1999, (C) 1999 Jochen Wiedmann";

=pod

=head1 NAME

  makerpm - Build binary distributions of Perl packages


=head1 SYNOPSYS

Extract the package sources:

  makerpm --prep --source=<package>-<version>.tar.gz

Compile the package:

  makerpm --build --package-name=<package> --package-version=<version>

Install the package into a build root directory:

  makerpm --install --package-name=<package> --package-version=<version>

Create a SPECS file by executing the above steps to create a file list:

  makerpm --specs --source=<package>-<version>.tar.gz


=head1 DESCRIPTION

The I<makerpm> 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</usr/src/redhat/SOURCES> and do a

  makerpm --specs --source=<package>-<version>.tar.gz

This will create a SPECS file in F</usr/src/redhat/SPECS> which you
can use with

  rpm -ba /usr/src/redhat/SPECS/<package>-<version>.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=<dir>

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</var/tmp/DBI-1.07>. Binaries are installed into F<$build_root/usr/bin>
rather than F</usr/bin>, man pages in F<$build_root/usr/man> and so on.

The idea is making the build process really reproducable 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=<msg>

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</var/tmp/build-root>)
is created and installation is adapted relative to that directory.
See the I<--build-root> option for details.

=item --make=<path>

Set path of the I<make> binary; defaults to the location read from Perl's
Config module. L<Config(3)>.

=item --makeopts=<opts>

Set options for running "make" and "make install"; defaults to none.

=item --makemakeropts=<opts>

If you need certain options for running "perl Makefile.PL", this is
your friend. By default no options are set.

=item --mode=<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<makerpm>, then
RPM mode is choosen. When running as I<makeppm>, then PPM mode is
enabled.

=item --package-name=<name>

=item --package-version=<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-base-dir=<dir>

=item --rpm-build-dir=<dir>

=item --rpm-source-dir=<dir>

=item --rpm-specs-dir=<dir>

Sets certain directory names related to RPM mode, defaults to
F</usr/src/redhat> (or F</usr/src/packages> on SuSE Linux),
F<$base_dir/BUILD>, F<$base_dir/SOURCES> and F<$base_dir/SPECS>.

=item --rpm-group=<group>

Sets the RPM group; defaults to Development/Languages/Perl.

=item --setup-dir=<dir>

Name of setup directory; defaults to <package>-<version>. The setup
directory is the name of the directory that is created by extracting
the sources. Example: DBI-1.07.

=item --source=<file>

Source file name; used to determine defaults for --package-name and
--package-version. This option is required for --specs and --prep.

=item --summary=<msg>

Summary line; defaults to "The Perl package <name>".

=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'};
    require Archive::Tar;
    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'};
    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";
    system $command;
}

sub ReadLocations {
    my %vars;
    my $fh = Symbol::gensym();
    open($fh, "<Makefile") || die "Failed to open Makefile: $!";
    while (my $line = <$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;
	my $fh = Symbol::gensym();
	open($fh, "+<$f") or die "Failed to open $File::Find::name: $!";
	local $/ = undef;
	my $contents = <$fh>;
	die "Failed to read $File::Find::name: $!" unless defined $contents;
	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'};
    system $command unless $self->{'debug'};

    print "Adjusting Paths in $self->{'build-root'}\n";
    $self->AdjustPaths($self->{'build-root'});
}

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();
    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'});
	$self->MakeInstall($self->{'setup-dir'});
    };
    my $status = $@;
    chdir $old_dir;
    die $@ if $status;
}


package Distribution::RPM;

@Distribution::RPM::ISA = qw(Distribution);

$Distribution::RPM::BASE_DIR = '/usr/src/redhat';
foreach my $dir (qw(/usr/src/redhat /usr/src/packages)) {
    if (-d $dir) {
	$Distribution::RPM::BASE_DIR = $dir;
	last;
    }
}
$Distribution::RPM::SOURCE_DIR = '/usr/src/redhat/SOURCES';
$Distribution::RPM::SPECS_DIR = '/usr/src/redhat/SPECS';
$Distribution::RPM::BUILD_DIR = '/usr/src/redhat/BUILD';

sub new {
    my $proto = shift;
    my $self = $proto->SUPER::new(@_);
    $self->{'rpm-base-dir'} ||= $Distribution::RPM::BASE_DIR;
    $self->{'rpm-source-dir'} ||= $Distribution::RPM::SOURCE_DIR;
    $self->{'rpm-specs-dir'} ||= $Distribution::RPM::SPECS_DIR;
    $self->{'rpm-build-dir'} ||= $Distribution::RPM::BUILD_DIR;
    $self->{'rpm-group'} ||= 'Development/Languages/Perl';
    push(@{$self->{'source_dirs'}}, $self->{'rpm-source-dir'});
    $self->{'build_dir'} = $self->{'rpm-build-dir'};
    $self;
}


sub Specs {
    my $self = shift;
    my $old_dir = Cwd::cwd();
    eval {
	$self->Prep();
	$self->Build();
	$self->Install();

	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, $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";
	}

	$specs .= <<"EOF";

%description
$self->{'summary'}

%prep
$makerpm_path --prep

%build
$makerpm_path --build %{makeopts} %{makemakeropts}

%install
rm -rf \$RPM_BUILD_ROOT
$makerpm_path --install %{makeopts}

%clean
rm -rf \$RPM_BUILD_ROOT

%files
EOF
        foreach my $dir (sort keys %dirs) {
	    next if $dirs{$dir};
	    $dir =~ s/^\Q$self->{'build-root'}\E//;
	    $specs .= "%dir $dir\n";
	}
	foreach my $file (sort keys %files) {
	    $file =~ s/^\Q$self->{'build-root'}\E//;
	    $specs .= "$file\n";
	}

	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,
					"<name>-<version>");
    my $start_perl = substr($Config::Config{'startperl'}, 2);

    print <<EOF;
Usage: $0 <action> [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=<dir>		Set build-root directory for installation;
				defaults to $build_root.
  --copyright=<msg>		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=<path>   		Set "make" path; defaults to $Config::Config{'make'}
  --makemakeropts=<opts>	Set options for running "perl Makefile.PL";
                                defaults to none.
  --makeopts=<opts>		Set options for running "make" and "make
                                install"; defaults to none.
  --mode=<mode>			Set build mode, defaults to $mode.
  --package-name=<name>		Set package name.
  --package-version=<name>	Set package version.
  --perl-path=<path>		Perl path to verify in generated scripts;
				defaults to $start_perl
  --require=<package>		Set prerequisite packages. May be used
				multiple times.
  --setup-dir=<dir>		Name of setup directory; defaults to
				<name>-<version>
  --source=<file>		Source file name; used to determine defaults
                                for <name> and <version>.
  --summary=<msg>		One line desription of the package; defaults
				to "The Perl package <name>".
  --verbose			Turn on verbose mode.
  --version			Print version string and exit.

Options for RPM mode are:

  --rpm-base-dir=<dir>		RPM base directory; defaults to
                                $Distribution::RPM::BASE_DIR.
  --rpm-build-dir=<dir>         RPM build directory; defaults to
      				$Distribution::RPM::BUILD_DIR.
  --rpm-group=<group>           RPM group, default Development/Languages/Perl.
  --rpm-source-dir=<dir>        RPM source directory; defaults to
                                $Distribution::RPM::SOURCE_DIR.
  --rpm-specs-dir=<dir>         RPM specs directory; defaults to
                                $Distribution::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',
			     '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 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<File::Spec> and C<Archive::Tar> packages.


=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-05-24  root  <joe@gate.ispsoft.de>

	* Added --perl-path and support for fixing startperl in scripts.
	  Some authors don't know how to fix it. :-(



=head1 SEE ALSO

L<ExtUtils::MakeMaker(3)>, L<rpm(1)>, L<ppm(1)>


=cut
