#!/usr/bin/perl -w # # Make an rpm out of a tarball. # # 30-Jun-2002 # Started making this actually work, although I'm sure there are # three hundred scripts to do this on Freshmeat. # 20/06/2003 # Allow --group # Clean up test area # use %configure and %makeinstall macros # 08/11/2003 # Use --eval instead of -E for rpm macro expansion # 18/02/2006 # Merge in rebuildspec code # # * Detects some configure scripts # * Tries to guess if %setup needs any parameters # * Tries to populate %doc # * Tries to locate GPL, Apache, or Artistic License # # Fixmes: # - check for existing specfile and use it for defaults # - use Archive::Tar to unpack # - Archive::Zip # - Compress::Zlib # - bzip handling also use strict; use File::Basename; use File::Copy; use File::Path; use Getopt::Long; use POSIX; use RPM::Specfile; use RPM2; my $group = ""; my $groupwith = ""; my $rpm = "/bin/rpm"; my $specdir = `$rpm --eval \%_specdir`; my $sourcedir = `$rpm --eval \%_sourcedir`; my $configure = `$rpm --eval \%configure`; my $dist = ""; my $release = 1; my @patches; my @obsoletes; my @ignorespec; chomp( $specdir ); chomp( $sourcedir ); my ( $forcename, $forcever ); GetOptions( 'group=s' => \$group, 'groupwith=s' => \$groupwith, 'group-with=s' => \$groupwith, # because I can never remember 'patch=s' => \@patches, 'ignorespec=s' => \@ignorespec, 'obsoletes=s' => \@obsoletes, 'distribution=s' => \$dist, 'name=s' => \$forcename, 'version=s' => \$forcever ) or die; # figure out what our distro is, approximately. if ( !$dist ) { if ( -f "/etc/redhat-release" ) { my $p = `rpm -qf /etc/redhat-release`; if ( `grep Enterprise /etc/redhat-release 2>/dev/null` ) { $dist = "rhel"; } elsif ( `grep Fedora /etc/redhat-release 2>/dev/null` ) { $dist = "fc"; } else { $dist = "rh"; } $dist .= `rpm -q --queryformat "\%{VERSION}" $p`; } else { $dist = "nodist"; } } $release .= ".$dist"; my $rpmspec = new RPM::Specfile; my $specfile = <<"EOT"; Summary: Name: Version: Release: $release License: Group: Source: #Patch: #Requires: BuildRoot: /var/tmp/%{name}-buildroot %description %prep %setup #%patch %build %configure make %install %makeinstall %clean /bin/rm -rf \$RPM_BUILD_ROOT %files %defattr(-,root,root) # %doc # %config EOT makespec( $ARGV[0], \@patches ); sub makespec { my ( $file, $patches ) = @_; my @spec; $file or die "Usage: $0 filename\n"; -f $file or die "$file not found\n"; if ( @{$patches} ) { for my $patch ( @{$patches} ) { if ( ! -f $patch ) { die "patch $patch not found\n"; } } } my ( $name, $version, $archive, $origfile, $summary, $havespec, $copyright ); $copyright = "Unknown"; $origfile = $file; $file = basename $file; # Kinda bogus, because tarfiles can have all sorts of stupid # naming conventions, but we'll start with this because it's # common enough. ( $name, $version, $archive ) = $file =~ m{^(.*?)-(\d+\..*)\.(tar\.(gz|Z|bz2)|tgz|zip)$}i; # try without a version if ( !defined( $name )) { ( $name, $archive ) = $file =~ m{^(.*?)\.(tar\.(gz|Z|bz2)|tgz|zip)$}i; $version = "1.0.0"; } $name = $forcename if $forcename; $version = $forcever if $forcever; die "Can't parse a name out of $file\n" unless $name; # Version can't contain - # (are other chars allowed?) $version =~ s/\-/_/g; # might try something clever with this some other time $summary = $name; print "Building a spec file for $name version $version\n"; # let's see if there's already a version of this package installed # - we can use it to set some defaults. In theory we should be # able to reconstruct most of the specfile. my $db = RPM2->open_rpm_db(); my $already = 0; if ( my $pkgs = $db->find_by_name_iter( $name )) { $already = $pkgs->next; # ignore all except the first } my $spec; if ( $already ) { print " You've already got a version of $name installed\n"; #$summary = `$rpm -q --queryformat \%{SUMMARY} $name 2>/dev/null`; $summary = $already->tag( 'SUMMARY' ); $spec = rebuildspec( $name ); } else { print " No previous installations found\n"; } if ( $groupwith ) { $group = `$rpm -q $groupwith --queryformat \%{GROUP} 2>/dev/null`; chomp( $group ); if ( $group =~ /package $groupwith is not installed/ ) { die "error: $group\n"; } else { print " Using group $group (same as $groupwith)\n"; } } # Groups can be obtained from /usr/share/doc/rpm-$version/GROUPS, # and the user prompted to select one or specify one on the # command line. my $rpmver = `$rpm -q --qf %{VERSION} rpm`; if ( open( GROUPS, "/usr/share/doc/rpm-$rpmver/GROUPS" )) { my @groups = ; close( GROUPS ); if ( !grep /^$group$/, @groups ) { if ( $already ) { $group = `$rpm -q $name --queryformat "%{GROUP}"`; print " extracted group $group from existing install\n"; } elsif ( $group ) { print STDERR "Unknown group '$group'\n"; print STDERR "Choose one from:\n\t" . join( "\t", @groups ) . "\n"; exit(1); } else { print STDERR "No group specified!\n"; print STDERR "Choose one from:\n\t" . join( "\t", @groups ) . "\n"; exit(1); } } } else { warn "Can't find/open GROUPS file: $!"; } # Patches can be specified on the command line. if ( @obsoletes ) { my $obsoletes = join( " ", @obsoletes ); $specfile =~ s/Source:/Source:\nObsoletes: $obsoletes/s; } # Look for licensing and other details my $buildroot = "/var/tmp/$ {name}-buildroot"; my $base = ""; my @docs; my $autoconf = ""; mkdir $buildroot, 0755 || die "Can't create $buildroot!\n"; if ( defined( $archive )) { print " Inspecting archive...\n"; my $compressor; my $archiver; my $lister; if ( $archive =~ /(tar\.(gz|Z)|tgz)/i ) { $archiver = "tar xf"; $compressor = "z"; $lister = "tar tf"; } elsif ( $archive =~ /tar.bz2$/i ) { $archiver = "tar xf"; $compressor = "j"; $lister = "tar tf"; } elsif ( $archive =~ /zip$/i ) { $archiver = "unzip "; $compressor = ""; $lister = "unzip -v"; } # check for a built-in spec file print " Checking for .spec files..."; my $cmd; if ( $lister !~ /unzip/ ) { $cmd = "$lister$compressor $origfile \"*.spec*\" 2>/dev/null"; $havespec = `$cmd`; } else { $havespec = ""; } for my $ignore ( @ignorespec ) { $havespec =~ s/\S+$ignore//; } $havespec =~ s/^\s*$//; if ( $havespec ) { chomp( $havespec ); if ( $havespec =~ /[\r\n]/ ) { # let's try and narrow the choices my @specs = grep /\.spec(\.in)?$/, split( /[\r\n]/, $havespec ); if ( $#specs == 1 ) { # favour the ".in" candidate if ( $specs[0] eq $specs[1] . ".in" ) { $havespec = $specs[0]; } elsif ( $specs[0] . ".in" eq $specs[1] ) { $havespec = $specs[1]; } else { die "multiple specfiles found, you're on your own:\n " . join( "\n ", $havespec, @specs ) . "\n"; } } else { die "multiple specfiles found, you're on your own.\n"; } } else { if ( $havespec =~ /\.spec.in/ ) { # need to generate the real .spec file print "found spec.in ($havespec)\n"; } else { print "found specfile ($havespec)\n"; } } } else { print "none found\n"; } # XXX this is pretty damned awful copy "$origfile", "$buildroot"; `cd $buildroot; $archiver$compressor $file`; # Look for common subdirectories - this will be used for %setup, # too. if ( -d "$buildroot/$name" ) { $base = "$name"; } elsif ( -d "$buildroot/$name-$version" ) { $base = "$name-$version"; } elsif ( -d "$buildroot/$ {name}_$version" ) { $base .= "$ {name}_$version"; } else { # Look for a lone directory in there opendir( BASEDIR, "$buildroot" ) or die "can't open $buildroot: $!"; my @dirs = grep !/^(\.\.?|$file)$/, readdir( BASEDIR ); closedir( BASEDIR ); if ( scalar( @dirs ) == 1 ) { $base = $dirs[0]; } else { print STDERR "Basedir ($buildroot) contents:\n"; print STDERR join( "\n", @dirs ) . "\n"; # die "Okay, make the goddamn base detection smarter."; print " I'm guessing it needs a directory created.\n"; } } if ( $base ) { print " Will build in BUILD/$base\n"; # parse out a new version! my $tmpver = $base; $tmpver =~ s/^$name//; $tmpver =~ s/^.*?([0-9].*)$/$1/; if ( $tmpver and $tmpver ne $version ) { print " New version string: $tmpver\n"; $version = $tmpver; } } # if we need to build a spec file, now would be a good time to do so. if ( $havespec =~ /\.spec.in/ ) { print " Building .spec from .spec.in..."; # .in suggests configure is present. However, we may also # need to do the autoconf dance of doom, etc. if ( ! -e "$buildroot/$base/configure" ) { if ( -e "$buildroot/$base/bootstrap" ) { `cd $buildroot/$base; ./bootstrap`; } } `cd $buildroot/$base; $configure`; $havespec =~ s/\.in$//; print "done\n"; } if ( $havespec ) { print " Checking provided specfile for sanity..."; open( SPEC, "<$buildroot/$havespec" ) or die "seems like it didn't generate ($buildroot/$havespec: $!)"; local $/ = undef; $specfile = ; close( SPEC ); # things that are liable to be hosed: mostly, just the # name of the source file. # unfortunately, use of macros can make this pretty sucky $specfile =~ s/^Source:.*$/Source: $file/m; ( $group ) = $specfile =~ m/^Group: (.*)$/m; $specfile =~ s/^Version: .*$/Version: $version/m; print "done\n"; } opendir( CONTENTS, "$buildroot/$base" ); my @files = grep !/^\.(\.)?$/, readdir CONTENTS; closedir( CONTENTS ); for my $f ( @files ) { # Generally, files in all-caps are documentation. Sometimes # they'll be called FILENAME.ext, though. if ( $f =~ /^[A-Z]+(\..*)?$/ ) { push @docs, $f; print " Found probable doc file $f\n"; if ( $f =~ /^COPYING(\.LIB)?$/ or $f eq "GPL" or $f eq "LICENSE" ) { if ( open( GPL, "$buildroot/$base/$f" )) { while () { if ( /Apache Software License/ ) { $copyright = "Apache Software License"; last; } next if /^\s*$/; if ( /GNU.*GENERAL PUBLIC LICENSE/ ) { print " This looks like the GPL or LGPL.\n"; $copyright = "GPL"; last; } } } if ( !$copyright ) { $copyright = "See $f for info"; } } } if ( $f eq "Artistic" ) { # XXX open it and have a look print " Found Artistic license\n"; $copyright = "Artistic"; push @docs, $f; } # Yay, it's a configure script! if ( $f eq "configure" ) { $autoconf = '%configure'; print " Found configure script\n"; } if ( $f eq "autogen.sh" ) { $autoconf = "./autogen.sh\n\%configure"; print " Found autogen script\n"; } if ( $f eq "Makefile.PL" ) { # shouldn't you be using cpanflute? $autoconf = "perl"; print " Found Makefile.PL\n"; } } # let's make any perl modules that aren't otherwise licensed # into Artistic licenses. if ( !$copyright and $autoconf eq "perl" ) { $copyright = "Artistic"; } if ( $autoconf eq "perl" ) { $autoconf = 'CFLAGS="$RPM_OPT_FLAGS" perl Makefile.PL PREFIX=$RPM_BUILD_ROOT%{_prefix}'; } print " Cleaning up inspection area..."; `/bin/rm -rf $buildroot`; print "done\n"; } # Summarize print "================================================================\n"; print <<"EOT"; Name: $name Version: $version Summary: $summary Group: $group License: $copyright EOT my $desc; if ( $already ) { $desc = `$rpm -q --queryformat \%{DESCRIPTION} $name 2>/dev/null`; } else { $desc = ""; } # make $specdir if it doesn't exist! -d $specdir or mkpath( [ $specdir ], 0, 0755 ); open( SPEC, ">$specdir/$name.spec" ) or die $!; if ( !$havespec ) { for my $line ( split /\n/, $specfile ) { $line =~ s/^Copyright:/License:/; $line =~ /^Summary:/and $line .= " $summary"; $line =~ /^Name:/ and $line .= " $name"; $line =~ /^Version:/ and $line .= " $version"; $line =~ /^Source:/ and $line .= " $file"; $line =~ /^License:/ and $line .= " $copyright"; $line =~ /^Group:/ and $line .= " $group"; # Fix the directory name if ( $line =~ /^\%setup/ ) { # If there's no base, then we need to fix that if ( !$base ) { $line .= " -n $name-$version -c"; } elsif ( $base and $base ne "$name-$version" ) { $line .= " -n $base"; } } if ( $line =~ /^\%description/ ) { $line .= "\n$desc" if $desc; } # Configure script found! if ( $line =~ /^%configure$/ ) { if ( !$autoconf ) { $line = "# UNKNOWN CONFIG METHOD\n"; } else { $line = $autoconf; } } # Add in the docs if ( $line =~ /^# \%doc/ ) { if ( @docs ) { $line = "\%doc " . join( " ", @docs ) . "\n"; } else { next; # don't print a %doc line } } print SPEC "$line\n"; } } else { print SPEC $specfile; } close SPEC; print "Wrote $name.spec\n"; # don't overwrite $origfile if $origfile is in $sourcedir if ( -f "$sourcedir/" . basename $origfile ) { print "It appears you already have a file in $sourcedir for this package; I'm not going to overwrite that.\n"; } else { if ( ! rename "$origfile", "$sourcedir/" . basename $file ) { copy( $origfile, "$sourcedir/" . basename $file ); } } } # This used be an entirely separate script... sub rebuildspec { my $file = shift; my $pkg; # Find out if this is an installed package or something on disk open( INFO, "$rpm -q $file|" ); my @info = ; close( INFO ); if ( grep /^package $file is not installed$/, @info ) { $pkg = "p"; } else { $pkg = ""; } # Handy variable to have around my $thispkg = `$rpm -q$pkg --queryformat '[%{NAME}-%{VERSION}]' $file`; # see if there's anything else built from the same src.rpm as this my @allpkgs = ( $thispkg ); my $srcrpm = `$rpm -q$pkg --queryformat '[%{SOURCERPM}]' $file`; print STDERR "Source came from $srcrpm, looking for other packages built from that...\n"; for my $line ( split( /\n/, `$rpm -qa --queryformat '%{SOURCERPM} %{NAME}-%{VERSION}\n'` )) { my ( $s, $p ) = split( ' ', $line ); if ( $s eq $srcrpm ) { push @allpkgs, $p unless grep /^$p$/, @allpkgs; } } # Some of this is bloody EVIL. my $format =<<"EOF"; Summary: %{SUMMARY} Name: %{NAME} Version: %{VERSION} Release: %{RELEASE} Copyright: %{COPYRIGHT}%|COPYRIGHT?{}:{ DELETEME}| Group: %{GROUP} Source: %{SOURCE}%|SOURCE?{}:{ DELETEME}| %|SOURCE?{ DELETEME}:{Source: $thispkg.tar.gz}| Url: %{URL}%|URL?{}:{ DELETEME}| Packager: %{PACKAGER}%|PACKAGER?{}:{ DELETEME}| Vendor: %{VENDOR}%|VENDOR?{}:{ DELETEME}| Requires: [%{REQUIRENAME} %{REQUIREFLAGS} %{REQUIREVERSION} ]%|REQUIRENAME?{}:{ DELETEME}| Provides: [%{PROVIDES} ] %%description %{DESCRIPTION} %%prep %%setup %%build %%install %%clean %%pre%|PREIN?{}:{ DELETEME}| %{PREIN}%|PREIN?{\n}:{ DELETEME}| %%post%|POSTIN?{}:{ DELETEME}| %{POSTIN}%|POSTIN?{\n}:{ DELETEME}| %%preun%|PREUN?{}:{ DELETEME}| %{PREUN}%|PREUN?{\n}:{ DELETEME}| %%postun%|POSTUN?{}:{ DELETEME}| %{POSTUN}%|POSTUN?{\n}:{ DELETEME}| %%files EOF print "Recovering huge lumpy chunks\n"; my @lines = grep !/ DELETEME$/, `$rpm -q$pkg --queryformat '$format' $file`; # MAGIC! @lines = map { if ( /^Requires\: / ) { s/\s+0\s+/ /g; s/12/>=/g; s/16396/>=/g; s/ (16384|64|1344|4416) //g; # no version specified s/(16777290|74)/<=/g; } $_; } @lines; # Ideally, we could slim down the above by figuring out what # provides each requirement, and sorting and uniquing that. # Now figure out the %files section print STDERR "recovering files list...\n"; my @lines2 = `$rpm -q$pkg --queryformat '[%{FILEFLAGS} %{FILENAMES}\n]' $file`; # Fix the flags for each file @lines2 = map { s|^2 (/usr/(share/)?doc/$thispkg/)?|\%doc |; s|^1 |\%config |; s|^0 ||; s|/usr/doc/$thispkg\n||; # throw away $_ } @lines2; # clean up the files list a bit my @docs = grep /^\%doc/, @lines2; my @files = grep !/^\%doc/, @lines2; my @man = grep /\/man\d/, @docs; @docs = grep !/\/man\d/, @docs; @man = map { s|\%doc ||; $_ } @man; push @lines, join( " ", "\%doc", map { s|^\%doc (.*)\n|$1|; $_ } @docs ) . "\n"; push @lines, ( @files, @man ); # add in the changelog push @lines, "\n"; push @lines, "\%changelog\n"; for my $cl ( split( '\n', `$rpm -q$pkg --queryformat '[* %{CHANGELOGTIME} %{CHANGELOGNAME}\n%{CHANGELOGTEXT}\n\n]' $file` )) { if ( $cl =~ /^\* (\d+) (.*)$/ ) { $cl = strftime( "* %a %b %e %Y $2", gmtime( $1 )); } push @lines, $cl . "\n"; } return join( '', @lines ); }