#!/usr/bin/env perl use strict; use warnings; use File::Find; use File::Spec; use Cwd; use Getopt::Long; use Pod::Usage; use Archive::Tar; use IPC::Open2; use POSIX; package NIC::Archive::Tar::File; use parent "Archive::Tar::File"; sub new { my $class = shift; my $self = Archive::Tar::File->new(@_); bless($self, $class); return $self; } sub full_path { my $self = shift; my $full_path = $self->SUPER::full_path(); $full_path = '' unless defined $full_path; $full_path =~ s#^#./# if $full_path ne "" && $full_path ne "." && $full_path !~ m#^\./#; return $full_path; } 1; package main; our $VERSION = '2.0'; our $_PROGNAME = "dm.pl"; my $ADMINARCHIVENAME = "control.tar.gz"; my $DATAARCHIVENAME = "data.tar"; my $ARCHIVEVERSION = "2.0"; our $compression = "gzip"; our $compresslevel = -1; Getopt::Long::Configure("bundling", "auto_version"); GetOptions('compression|Z=s' => \$compression, 'compress-level|z=i' => \$compresslevel, 'build|b' => sub { }, 'help|?' => sub { pod2usage(1); }, 'man' => sub { pod2usage(-exitstatus => 0, -verbose => 2); }) or pod2usage(2); pod2usage(1) if(@ARGV < 2); if($compresslevel < 0 || $compresslevel > 9) { $compresslevel = 6; $compresslevel = 9 if $compression eq "bzip2"; } if(($compression eq "bzip2" || $compression eq "gzip") && $compresslevel eq 0) { $compresslevel = 1; } my $pwd = Cwd::cwd(); my $indir = File::Spec->rel2abs($ARGV[0]); my $outfile = $ARGV[1]; die "ERROR: '$indir' is not a directory or does not exist.\n" unless -d $indir; my $controldir = File::Spec->catpath("", $indir, "DEBIAN"); die "ERROR: control directory '$controldir' is not a directory or does not exist.\n" unless -d $controldir; my $mode = (lstat($controldir))[2]; die sprintf("ERROR: control directory has bad permissions %03lo (must be >=0755 and <=0775)\n", $mode & 07777) if(($mode & 07757) != 0755); my $controlfile = File::Spec->catfile($controldir, "control"); die "ERROR: control file '$controlfile' is not a plain file\n" unless -f $controlfile; my %control_data = read_control_file($controlfile); die "ERROR: control file '$controlfile' is missing a Package field" unless defined $control_data{"package"}; die "ERROR: control file '$controlfile' is missing a Version field" unless defined $control_data{"version"}; die "ERROR: control file '$controlfile' is missing an Architecture field" unless defined $control_data{"architecture"}; die "ERROR: package name has characters that aren't lowercase alphanums or '-+.'.\n" if($control_data{"package"} =~ m/[^a-z0-9+-._]/); die "ERROR: package version ".$control_data{"version"}." doesn't contain any digits.\n" if($control_data{"version"} !~ m/[0-9]/); foreach my $m ("preinst", "postinst", "prerm", "postrm", "extrainst_") { $_ = File::Spec->catfile($controldir, $m); next unless -e $_; die "ERROR: maintainer script '$m' is not a plain file or symlink\n" unless(-f $_ || -l $_); $mode = (lstat)[2]; die sprintf("ERROR: maintainer script '$m' has bad permissions %03lo (must be >=0555 and <=0775)\n", $mode & 07777) if(($mode & 07557) != 0555) } if (-d "$outfile") { $outfile = sprintf('%s/%s_%s_%s.deb', $outfile, $control_data{"package"}, $control_data{"version"}, $control_data{"architecture"}); } print "$_PROGNAME: building package `".$control_data{"package"}.":".$control_data{"architecture"}."' in `$outfile'\n"; open(my $ar, '>', $outfile) or die $!; print $ar "!\n"; print_ar_record($ar, "debian-binary", time, 0, 0, 0100644, 4); print_ar_file($ar, "$ARCHIVEVERSION\n", 4); { my $tar = Archive::Tar->new(); $tar->add_files(tar_filelist($controldir)); my $comp; my $zFd = IO::Compress::Gzip->new(\$comp, -Level => 9); $tar->write($zFd); $zFd->close(); print_ar_record($ar, $ADMINARCHIVENAME, time, 0, 0, 0100644, length($comp)); print_ar_file($ar, $comp, length($comp)); } { my $tar = Archive::Tar->new(); $tar->add_files(tar_filelist($indir)); my ($fh_out, $fh_in); my $pid = open2($fh_out, $fh_in, compression_cmd()) or die "ERROR: open2 failed to create pipes for '$::compression'\n"; fcntl($fh_out, F_SETFL, O_NONBLOCK); fcntl($fh_in, F_SETFL, O_NONBLOCK); my $tmp_data = $tar->write(); my $tmp_size = length($tmp_data); my ($off_in, $off_out) = (0, 0); my ($archivedata, $archivesize); while($off_in < $tmp_size) { my ($rin, $win) = ('', ''); my ($rout, $wout); vec($win, fileno($fh_in), 1) = 1; vec($rin, fileno($fh_out), 1) = 1; # Wait for space to be available to write or data to be available to read select($rout=$rin, $wout=$win, undef, undef); if (vec($wout, fileno($fh_in), 1)) { # Write 8KB of data my $wrote = syswrite $fh_in, $tmp_data, 8192, $off_in; $off_in += $wrote if (defined $wrote); } if (vec($rin, fileno($fh_out), 1)) { # Get the compressed result if possible my $o = sysread $fh_out, $archivedata, 8192, $off_out; $off_out += $o if (defined($o)); } } $fh_in->close(); while (1) { # Get the remaining data my $o = sysread $fh_out, $archivedata, 8192, $off_out; if (defined($o) && $o > 0) { $off_out += $o; } elsif ($! != EAGAIN) { last; } } $archivesize = $off_out; $fh_out->close(); waitpid($pid, 0); print_ar_record($ar, compressed_filename($DATAARCHIVENAME), time, 0, 0, 0100644, $archivesize); print_ar_file($ar, $archivedata, $archivesize); } close $ar; sub print_ar_record { my ($fh, $filename, $timestamp, $uid, $gid, $mode, $size) = @_; printf $fh "%-16s%-12lu%-6lu%-6lu%-8lo%-10ld`\n", $filename, $timestamp, $uid, $gid, $mode, $size; $fh->flush(); } sub print_ar_file { my ($fh, $data, $size) = @_; syswrite $fh, $data; print $fh "\n" if($size % 2 == 1); $fh->flush(); } sub tar_filelist { my $dir = getcwd; chdir(shift); my @filelist; my @symlinks; find({wanted => sub { return if m#^./DEBIAN#; my $tf = NIC::Archive::Tar::File->new(file=>$_); my $mode = (lstat($_))[2] & 07777; $tf->mode($mode); $tf->chown("root", "wheel"); push @symlinks, $tf if -l; push @filelist, $tf if ! -l; }, no_chdir => 1}, "."); chdir($dir); return (@filelist, @symlinks); } sub read_control_file { my $filename = shift; open(my $fh, '<', $filename) or die "ERROR: can't open control file '$filename'\n"; my %data; while(<$fh>) { die "ERROR: control file contains Windows/Macintosh line endings - please use a text editor or dos2unix to change to Unix line endings\n" if(m/\r/); if(m/^(.*?): (.*)/) { $data{lc($1)} = $2; } } close $fh; return %data; } sub compression_cmd { return "gzip -c".$compresslevel if $::compression eq "gzip"; return "bzip2 -c".$compresslevel if $::compression eq "bzip2"; return "lzma -c".$compresslevel if $::compression eq "lzma"; return "xz -c".$compresslevel if $::compression eq "xz"; if($::compression ne "cat") { print "WARNING: compressor '$::compression' is unknown, falling back to cat.\n"; } return "cat"; } sub compressed_filename { my $fn = shift; my $suffix = ""; $suffix = ".gz" if $::compression eq "gzip"; $suffix = ".bz2" if $::compression eq "bzip2"; $suffix = ".lzma" if $::compression eq "lzma"; $suffix = ".xz" if $::compression eq "xz"; return $fn.$suffix; } __END__ =head1 NAME dm.pl =head1 SYNOPSIS dm.pl [options] =head1 OPTIONS =over 8 =item B<-b> This option exists solely for compatibility with dpkg-deb. =item B<-ZEcompressionE> Specify the package compression type. Valid values are gzip (default), bzip2, lzma, xz and cat (no compression.) =item B<-zEcompress-levelE> Specify the package compression level. Valid values are between 0 and 9. Default is 9 for bzip2, 6 for others. 0 is identical to 1 when using bzip2 and gzip. Refer to B, B, B for explanations of what effect each compression level has. =item B<--help>, B<-?> Print a brief help message and exit. =item B<--man> Print a manual page and exit. =back =head1 DESCRIPTION B creates Debian software packages (.deb files) and is a drop-in replacement for dpkg-deb. =cut