#!/usr/bin/perl -w
# debbuild script
# Shamelessly steals interface from rpm's "rpmbuild" to create
# Debian packages.  Please note that such packages are highly
# unlikely to conform to "Debian Policy".
#
# Copyright (C) 2005-2015 Kris Deugau <kdeugau@deepnet.cx>
# Copyright (C) 2015 Andreas Scherer <https://ascherer.github.io/>
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

use strict;
use warnings;

use Cwd qw(abs_path);	# for finding where files really are
use Fcntl;		# for sysopen flags
use File::Basename;
use Getopt::Long qw(:config no_ignore_case bundling);
use Getopt::Std;
use Text::Balanced qw(extract_bracketed extract_multiple);

my $version = '16.6.1';

# Behavioural compatibility FTW!  Yes, rpmbuild does this too.
die "No .spec file to work with!  Exiting.\n" unless @ARGV;

# Initialized globals
my $verbosity = 0;
my $NoAutoReq = 0;
my %cmdopts = (type => '',
		stage => 'a',
		short => undef);
my %defattr = (filemode => '-',
		owner => '-',
		group => '-',
		dirmode => '-');

# Scriptlets
my %script = ( prep => '',
		build => '',
		install => '',
		check => '',
		clean => qq([ "\$RPM_BUILD_ROOT" != "/" ] && %{__rm} -rf \$RPM_BUILD_ROOT\n) );
my $finalmessages = ''; # A place to stuff messages that I want printed at the *very* end of any processing.

# User's prefs for dirs, environment, etc,etc,etc.
# config file ~/.debmacros
# Default ordered search paths for config/macros:
# /usr/lib/rpm/rpmrc  /usr/lib/rpm/redhat/rpmrc  /etc/rpmrc      ~/.rpmrc
# /usr/lib/rpm/macros /usr/lib/rpm/redhat/macros /etc/rpm/macros ~/.rpmmacros
# **NOTE:  May be possible to (ab)use bits of debhelper
my %specglobals = ( # For %define's in specfile, among other things.
	# this can be changed by the Vendor: header in the spec file
	vendor => 'debbuild');
my %macroopts; # For macro options

# Package data
# This is the form of $pkgdata{pkgname}{meta}
# meta includes Summary, Name, Version, Release, Group, Copyright,
#	Source, URL, Packager, BuildRoot, Description, BuildRequires,
#	Requires, Provides
# 10/31/2005 Maybe this should be flatter?  -kgd
my %pkgdata = (main => {source => ''});
my @pkglist = ('main');	#sigh
# Files listing.  Embedding this in %pkgdata would be, um, messy.
my %filelist;
my %doclist;
my @buildreq;

# "Constants"
my %targets = ('p' => 'Prep',
		'c' => 'Compile',
		'i' => 'Install',
		'l' => 'Verify %files',
		'a' => 'Build binary and source',
		'b' => 'Build binary',
		's' => 'Build source');
# Ah, the joys of multiple architectures.  :(  Feh.
# As copied from rpm
my %optflags = ( 'i386' => '-O2 -g -march=i386 -mcpu=i686',
		'amd64'	=> '-O2 -g',
		  'all'	=> '');

# Hackery to try to bring some semblance of sanity to packages built for more
# than one Debian version at the same time.  Whee.
# /etc/debian-version and/or version of base-files package
my %distmap = (
# legacy Unbuntu
	'3.1.9ubuntu'	=> 'dapper',
	'6.06'		=> 'dapper',
	'4ubuntu'	=> 'feisty',
	'7.04'		=> 'feisty',
        '4.0.1ubuntu'	=> 'hardy',
	'8.04'		=> 'hardy',
# Note we do NOT support identification of "subrelease" versions (ie semimajor updates
# to a given release).  If your dependencies are really that tight, and you can't rely
# on the versions of the actual dependencies, you're already in over your head, and
# should probably only ship a tarballed installer.
# only supporting LTS releases
# base-files version doesn't map to the Ubuntu version the way Debian versions do, thus the doubled entries
#	Ubuntu 12.04.5 LTS (Precise Pangolin)
	'6.5ubuntu'	=> 'precise',
	'12.04'		=> 'precise',
#	Ubuntu 14.04.2 LTS (Trusty Tahr)
	'7.2ubuntu'	=> 'trusty',
	'14.04'		=> 'trusty',
#	Ubuntu 16.04.0 LTS (Xenial Xerus)
	'9.4ubuntu'	=> 'xenial',
	'16.04'		=> 'xenial',
# Debian releases
	'3.0'	=> 'woody',
	'3.1'	=> 'sarge',
	'4'	=> 'etch',
	'5'	=> 'lenny',
	'6'	=> 'squeeze',
	'7'	=> 'wheezy',
	'8'	=> 'jessie',
	'9'	=> 'stretch',
	'99'	=> 'sid');

# Enh.  There doesn't seem to be any better way to do this...  :(
{
# could theoretically also do something with this...  it's about as stable as "dpkg-query ..." below...  :(
#  chomp( my $releasever = qx ( $specglobals{__cat} /etc/debian_version ) );
  my $basefiles;
  my $basever;
  my $baseos;

  # Funny thing how files like this have become useful...
  # Check for /etc/os-release.  If that doesn't exist, try /etc/lsb-release.  If that doesn't exist,
  # check for existence of dpkg-query, and either call it Debian Woody (if missing), or fall through
  # to guessing based on the version of the base-files package.
  if (open OSREL, '/etc/os-release') {
    # Look for ID and VERSION_ID lines.
    while (<OSREL>) {
      $baseos = $1 if /^ID="?(\w+)"?/;
      $basever = $1 if /^VERSION_ID="?([\d.]+)"?/;
    }
    close OSREL;

  } elsif (open LSBREL, '/etc/lsb-release') {
    # Look for DISTRIB_ID and DISTRIB_RELEASE lines.
    # We could also theoretically extract the dist codename, but since we have to hand-map
    # it in so many other cases there's little point.
    while (<LSBREL>) {
      $baseos = $1 if /^DISTRIB_ID="?(\w+)"?/;
      $basever = $1 if /^DISTRIB_RELEASE="?([\d.]+)"?/;
    }
    close LSBREL;

  } elsif (-e $specglobals{__dpkg_query}) {
# *eyeroll*  there *really* has to be a better way to go about this.  You
# can't sanely build packages for multiple distro targets if you can't
# programmatically figure out which one you're building on.

# note that we care only about major release numbers; tracking minor or point
# releases would be...  exponentially more painful.

    # for the lazy copy-paster:  dpkg-query --showformat '${version}\n' -W base-files
    # avoid shellisms
    if (open BASEGETTER, '-|', $specglobals{__dpkg_query}, '--showformat', '${version}', '-W', 'base-files') {
      $basever = <BASEGETTER>;
      close BASEGETTER;

      if ($basever =~ /ubuntu/) {
        # Ubuntu, at least until they upset their versioning scheme again
        # note that we remap the basefiles version to the public release number, to match the
        # behaviour for Debian, and to match the unofficial standard for RHEL/Centos etc and Fedora
        ($basefiles,$basever,$baseos) = $basever =~ /^(([\d.]+)(ubuntu))\d/;
      } else {
        # Debian, or more "pure" derivative
        $baseos = 'debian';
        ($basever,my $majver) = $basever =~ /^((\d+)(?:\.\d)?)/;
        $basever = $majver if $majver > 3;
      }
    }

    if (not $basever) {
      # Your llama is on fire
      $basever = '99';
      warn "Warning:  couldn't autodetect OS version, assuming sid/unstable\n";
    }
  } else { # got dpkg-query?
    # call it woody, since sarge and newer have dpkg-query, and we don't much care about obsolete^n releases
    $basever = '3.0';
    $baseos = 'debian';
  }

  # rpmbuild expects either integers or strings, and some distros (*cough*Ubuntu*cough*)
  # have versions that get interpreted as strings, which creates comparison problems.
  # This will make sure it's an integer to remain compatible.
  (my $specbasever = $basever) =~ s/\.//;

  # Set some legacy globals.
  $specglobals{debdist} = $distmap{$basever};
  $specglobals{debver} = $specbasever;

  # Set the standard generic OS-class globals;
  $baseos = lc $baseos;
  $specglobals{$baseos} = $specbasever;

  # Default %{dist} to something marginally sane.  Note this should be overrideable by --define.
  # This has been chosen to most closely follow the usage in RHEL/CentOS and Fedora, ie "el5" or "fc20".
  $specglobals{dist} = $baseos.$basever;

} # done trying to set debian dist/version


#### main ####

# Program flow:
# -> Parse/execute "system" config/macros (if any - should be rare)
# -> Parse/execute "user" config/macros (if any - *my* requirement is %_topdir)
# -> Parse command line for options, spec file/tarball/.src.deb (NB - also accept .src.rpm)

load_config();
parse_cmd();

# global shortcuts
my $topdir = expandmacros('%{_topdir}');

if ($cmdopts{type} eq 's') {
  install_sdeb($specglobals{srcpkg});
  exit 0;
}

# output stage of --showpkgs
if ($cmdopts{type} eq 'd') {
  parse_spec($specglobals{specfile});
  foreach my $pkg (@pkglist) {
    print format_debfile($pkg)."\n" if $filelist{$pkg};
  }
  # Source package
  print format_sdebfile()."\n";
  exit 0;
}

# Stick --rebuild handling in here - basically install_sdeb()
# followed by tweaking options to run with -ba
# --recompile is the same, except it reconfigures -bi
if ($cmdopts{type} eq 'r') {
  my $specfile;
  if ($specglobals{srcpkg} =~ /\.src\.rpm$/) {
    ($specfile) = grep { /\.spec/ } qx ( $specglobals{__rpm} -qlp $specglobals{srcpkg} );
    qx ( $specglobals{__rpm} -i $specglobals{srcpkg} );
  } elsif ($specglobals{srcpkg} =~ /\.sdeb$/) {
    install_sdeb($specglobals{srcpkg});
    $specfile = basename( qx ( $specglobals{__pax} -f $specglobals{srcpkg} *.spec ) );
  } else {
    die "Can't --rebuild with $specglobals{srcpkg}\n";
  }
  chomp( $specglobals{specfile} = "$topdir/SPECS/$specfile" );
  $cmdopts{type} = 'b'; # fall through
}

if ($cmdopts{type} eq 't') {
  # Need to inspect the tarball to find the spec file.
  # Note that rpmbuild doesn't seem to support this operation from a
  # .zip file properly, but we try our best.
  die "No tarfile specified!  Exiting.\n" unless defined $specglobals{tarball};
  my $tarball = $specglobals{tarball};
  my $cmdline = expandmacros(lookup_specfile($tarball));
  chomp( $specglobals{specfile} = "$topdir/SPECS/".basename( qx { $cmdline } ) );

  $tarball = abs_path($tarball);
  system expandmacros(extract_specfile($tarball));

  $cmdopts{type} = 'b'; # fall through
}

if ($cmdopts{type} eq 'b') {
  # Need to read the spec file to find the tarball.  Note that
  # this also generates most of the shell script required.
  parse_spec($specglobals{specfile});
  die "Can't build $pkgdata{main}{name}:  build requirements not met.\n"
    unless checkbuildreq();
  exit 0 if $specglobals{NOBUILD};

  # Expand macros as necessary.
  $specglobals{buildroot} = $specglobals{CBR} if defined $specglobals{CBR};
  $specglobals{buildroot} = expandmacros($specglobals{buildroot});
}

# work out the [pcilabs] stages
if ($cmdopts{stage} eq 'p' || ($cmdopts{stage} =~ /[cilab]/ and not $cmdopts{short})) {
  execute_script('prep');
}
if ($cmdopts{stage} eq 'c' || ($cmdopts{stage} =~ /[ilab]/ and not $cmdopts{short})) {
  execute_script('build');
}
if ($cmdopts{stage} =~ /[ilab]/) {
  install();
}
if ($cmdopts{stage} =~ /[as]/) {
  srcpackage();
}
if ($cmdopts{stage} =~ /[ab]/) {
  binpackage();
  execute_script('clean');
}

# Spit out any closing remarks
print $finalmessages;

# Just in case.
exit 0;

#### end main ####


## load_config()
# Load system macros similar to RPM which digests
# /usr/lib/rpm/macros /usr/lib/rpm/redhat/macros /etc/rpm/macros
# and user configuration (if any) ~/.rpmmacros a.k.a. ~/.debmacros
sub load_config {
  # Load user configuration, permitting local override
  my $homedir = $ENV{HOME} // $ENV{LOGDIR} // (getpwuid($<))[7];
  foreach my $macros ( ('/usr/lib/debbuild/macros',
                        </usr/lib/debbuild/macros.d/macros.*>,
                        '/etc/debbuild/macros',
                        </etc/debbuild/macros.*>,
                        "$homedir/.debmacros") ) {
    open MACROS,$macros or next; # should we warn about missing macro files?
    while (<MACROS>) {
      next unless my ($macro,$eq,$value) = /^%(\w+(?:\([\w:]*\))?)(=|\s*)(.+)$/;
      if ($value =~ s/\\$//) { # multi-line macro
        $value .= "\\\n" if $value ne '';
        while (<MACROS>) {
          $value .= $_;
          last unless /\\$/;
        }
      }
      $macroopts{$macro} = $1 if $macro =~ s/\((.*)\)//;
      chomp( $specglobals{$macro} = $eq eq '=' ? $specglobals{$value} : $value );
    }
    close MACROS;
  }
} # end load_config()


## parse_cmd()
# Parses command line into global hash %cmdopts, other globals
# Options based on rpmbuild's options
sub parse_cmd {
  # Don't feel like coding my own option parser...
  Getopt::Long::GetOptions(
    'buildroot=s'   => \$specglobals{CBR},
    'eval=s'        => sub { print expandmacros($_[1])."\n" },
    'short-circuit' => \$cmdopts{short},
    'showpkgs'      => sub { $cmdopts{type} = 'd' },
    'noprep'        => \$specglobals{NOPREP},
    'nobuild'       => \$specglobals{NOBUILD},
    'nocheck'       => \$specglobals{NOCHECK},
    'noclean'       => \$specglobals{NOCLEAN},
    'v+' => \$verbosity, # bump verbosity.  Not sure what I'll actually do here...
    'rebuild=s'   => \&srcpkg_handler,
    'recompile=s' => \&srcpkg_handler,
    'install|i=s' => \&srcpkg_handler,
    'b=s'         => \&build_handler, # do NOT use 'b|t=s' here!
    't=s'         => \&build_handler, # see 'build_handler' for details
    'r=s'         => \&build_handler,
    'with=s'      => \&define_handler, # dito for 'with|without=s'
    'without=s'   => \&define_handler,
    'define|D=s'  => \&define_handler,
    'help|?'      => \&help_handler,
    'version'     => sub { print "debbuild v$version\n" },
    '<>'          => \&catchall # process non-option arguments
  ); # Getopt::Long::Getoptions()
  ## catchall()
  # --buildroot, --define|-D, --rebuild, --recompile, and --install|-i are the
  # only options that take an argument.  Therefore, any *other* bare arguments
  # are the spec file or the tarball we're operating on - depending on which
  # one we meet.
  sub catchall {
    my ($opt_arg) = @_;
    if ($cmdopts{type} eq 'b' || $cmdopts{type} eq 'd') {
      # Spec file
      $specglobals{specfile} = $opt_arg;
    } elsif ($cmdopts{type} eq 't') {
      # Tarball build.  Need to extract tarball to find spec file.  Whee.
      $specglobals{tarball} = $opt_arg;
    } else {
      # Source package
      $specglobals{srcpkg} = $opt_arg;
    }
  }
  ## srcpkg_handler()
  # prepare $specglobals{srcpkg} in dependence of the calling option
  sub srcpkg_handler {
    my ($opt_name,$opt_value) = @_;
    $specglobals{srcpkg} = $opt_value;
    $cmdopts{type} = 'r';
    if ($opt_name eq 'rebuild') {
      $cmdopts{stage} = 'b';
    } elsif ($opt_name eq 'recompile') {
      $cmdopts{stage} = 'i';
    } else { # $opt_name eq 'install'
      $cmdopts{type} = 's';
    }
  }
  ## build_handler()
  # You can't use 'b|t=s' in Getopt::Long::GetOptions(), because 't' will be
  # treated as an 'alias' to the 'primary' option 'b' and will NOT receive its
  # own $opt_name.  We have to factor-out the handler and use _two_ options.
  sub build_handler {
    my ($opt_name,$opt_value) = @_;
    die "Unknown stage '$opt_value' for option '-$opt_name'.\n"
      unless grep { /$opt_value/ } keys %targets;
    if ($cmdopts{type} eq 'r') {
      # Mutually exclusive options.
      die "Can't use -$opt_name$opt_value with --rebuild\n";
    } else {
      $cmdopts{type} = $opt_name;
      $cmdopts{stage} = $opt_value;
    }
  }
  ## define_handler()
  sub define_handler {
    my ($opt_name,$opt_value) = @_;
    if ($opt_name =~ /with/) {
      # create 'configure' options from '--with <flag>' and '--without <flag>':
      # 'with/without <flag>' are 'aliases' for 'define _with_<flag>'.
      $opt_value = "\_$opt_name\_$opt_value --$opt_name-$opt_value";
      $opt_name = 'define'; # fall through with generic name
    }
    my ($macro,$value) = $opt_value =~ m/([a-z0-9_.-]+)(?:\s+(.+))?/i;
    if (defined $value) {
      store_value($macro,$value);
    } else {
      warn "WARNING: missing value for macro $macro in --$opt_name! Ignoring.\n";
    }
  }
  ## help_handler()
  # Factor-out the 'help' text. Could as well be SYNOPSIS in pod.
  sub help_handler {
    print "debbuild v$version".q{
Copyright 2005-2015 Kris Deugau <kdeugau@deepnet.cx>
Copyright 2015 Andreas Scherer <https://ascherer.github.io/>

Build .deb packages from RPM-style .spec files
debbuild supports most package-building options rpmbuild does.

Build options with [ <specfile> | <tarball> | <source package> ]:
      -b.                        build from <specfile> ...
      -t.                        build from <tarball> ...
      -r.                        build from <source package> ...
            -.p      ... through %prep (unpack sources and apply patches)
            -.c      ... through %build (%prep, then compile)
            -.i      ... through %install (%prep, %build, then install)
            -.l      verify %files section
            -.a      ... source and binary packages
            -.b      ... binary package only
            -.s      ... source package only
      --rebuild (-rb)            build binary package from <source package>
      --recompile (-ri)          build through %install from <source package>
      --buildroot=DIRECTORY      override build root
      --short-circuit            skip straight to specified stage (only for c,i)

Common options:
  -D, --define='MACRO EXPR'      define MACRO with value EXPR
      --with/--without FLAG      define build conditionals from FLAG

debbuild-specific options:
      -i, --install              Unpack a .sdeb in the %_topdir tree
      --showpkgs                 Show package names that would be built.
      --nobuild                  Parse <specfile>, but do no processing
};
    exit 0;
  }

  # Some cross-checks.  rpmbuild limits --short-circuit to just
  # the "compile" and "install" targets - with good reason IMO.
  # Note that --short-circuit with -.p is not really an error, just redundant.
  # NB - this is NOT fatal, just ignored!
  if ($cmdopts{short} && $cmdopts{stage} =~ /[labs]/) {
    warn "Can't use --short-circuit for $targets{$cmdopts{stage}} stage.  Ignoring.\n";
    $cmdopts{short} = undef;
  }

  # Did we catch an action option?
  # rpmbuild quits silently.
  exit 0 unless $cmdopts{type};
} # end parse_cmd()


## parse_spec()
# Parse the .spec file. This is a single loop, where we scan for '%commands'
# and '%{macros}'. The latter are usually treated in 'expandmacros()', with
# a little overlap here and there.
sub parse_spec {
  my ($specfile, $stage, $subname, $scriptlet) = @_;
  $stage //= 'preamble';
  $subname //= 'main';

  die "No .spec file specified!  Exiting.\n" unless $specfile;
  open my $fh, $specfile or die "specfile ($specfile) barfed: $!\n";

  $pkgdata{main}{arch} //= expandmacros('%{_arch}');

  my @ifexpr = (); # Nested %if..%else..%endif conditionals

# Basic algorithm:
# For each line
#   if it's a member of an %if construct, branch and see which segment of the
#	spec file we need to parse and which one gets discarded, then
#	short-circuit back to the top of the loop.
#   if it's a %section, bump the stage.  Preparse addons to the %section line
#	(eg subpackage) and stuff them in suitable loop-global variables, then
#	short-circuit back to the top of the loop.
#   Otherwise, parse the line according to which section we're supposedly
#	parsing right now

LINE: while (<$fh>) {
    next if /^\s*#/ and $stage !~ /changelog|copyrightdata/; # Ignore comments...
    next if /^\s*$/ and $stage !~ /changelog|copyrightdata|description/; # ... and blank lines.

    # need to deal with these someday
    next if /^%verify/ or /^%ghost/;

# no sense in continuing if we find something we don't grok
    # Yes, this is really horribly fugly.  But it's a cheap crosscheck against invalid
    # %-tags which also make rpmbuild barf.  In theory.
# notes:  some of these are not *entirely* case-sensitive (%ifxxx), but most are.
    # Extracted from the Maximum RPM online doc via:
    # grep -h %[a-z] *|perl -e 'while (<>) { /%([a-z0-9]+)\b/; print "$1|\n"; }'|sort -u
    if (/^%([a-z_]+)/ and not grep { /$1/ } qw(attr autopatch autosetup
	bcond_with bcond_without build changelog check clean config
	configure copyrightdata defattr define description dir doc docdir
	dump else endif exclude files ghost global if ifarch ifnarch ifnos ifos
	include install make_build make_install makeinstall package patch
	post postun pre prep preun readme setup triggerin triggerpostun
	triggerun undefine verify verifyscript)) {
      if (defined $specglobals{$1}) {
        # This looks like a user-defined macro, possibly with arguments.
        # Wrap the whole thing in curly braces for easier processing.
        s/^%(.+)$/%{$1}/;
      } else {
        die "Unknown tag '%$1' at line $. of $specfile\n";
      }
    }

    # RPM conditionals - transform to generic form
    if (s/^%if(arch|os)\s+//) {
      my @args = map { "%{_$1}==$_" } split /[\s,]+/;
      $_ = '%if ' . join '||', @args;
    }
    if (s/^%ifn(arch|os)\s+//) {
      my @args = map { "%{_$1}!=$_" } split /[\s,]+/;
      $_ = '%if ' . join '&&', @args;
    }

    # Generic %if..%else..%endif construct
    if (s/^%if//) {
      chomp( my $expr = lc expandmacros($_) );

      if ($expr =~ /^[\d\s<=>&|\(\)+-]+$/) {
        # "plain" numeric expressions are evaluated as-is, except
        $expr =~ s/(\D)0(\d+)/$1$2/g; # shortcut 0%{?ubuntu} == 1204
      } else {
        # got a logic statement we want to turn into a 1 or a 0.
        # correctly "quote" the Boolean variables for Perl's eval below
        $expr =~ s/"//g;
        $expr =~ s/(\w+)/"$1"/g;

        # Done in this order so we don't cascade incorrectly.
        # Yes, those spaces ARE correct in the replacements!
        $expr =~ s/==/ eq /g;
        $expr =~ s/!=/ ne /g;
        $expr =~ s/<=>/ cmp /g;
        $expr =~ s/<=/ le /g;
        $expr =~ s/>=/ ge /g;
        $expr =~ s/</ lt /g;
        $expr =~ s/>/ gt /g;
      }

      # http://www.donath.org/Quotes/AllTruthIsOne/
      unshift @ifexpr, (eval $expr or 0);

      next LINE if $ifexpr[0]; # This appears to be the only case we call false.
      my $iflevel = @ifexpr;
      while (<$fh>) { # Skip %if-block, inluding nested %if..%else..%endif
        if (/^%if/) {
          $iflevel++;
        } elsif (/^%else/) {
          goto ELSE if $iflevel == @ifexpr;
        } elsif (/^%endif/) {
          goto ENDIF if $iflevel == @ifexpr;
          $iflevel--;
        }
      }
      die "Unmatched %if at end of file. Missing %else/%endif.\n";
    }
ELSE: if (/^%else/) {
      die "Unmatched %else in line $.. Missing %if.\n" unless @ifexpr;
      next LINE unless $ifexpr[0];
      my $iflevel = @ifexpr;
      while (<$fh>) { # Skip %else-block, inluding nested %if..%else..%endif
        if (/^%if/) {
          $iflevel++;
        } elsif (/^%else/) {
          goto ELSE if $iflevel == @ifexpr;
        } elsif (/^%endif/) {
          goto ENDIF if $iflevel == @ifexpr;
          $iflevel--;
        }
      }
      die "Unmatched %else at end of file. Missing %endif.\n";
    }
ENDIF: if (/^%endif/) {
      die "Unmatched %endif in line $.. Missing %if/%else.\n" unless @ifexpr;
      shift @ifexpr;
    } # %if..%else..%endif

### Diagnostics '%{macros}'; these should actually be handled in expandmacros()
    elsif (/^%\{echo:(.+)}/) {
      print expandmacros($1)."\n";
    } elsif (/^%\{warn:(.+)}/) {
      warn expandmacros($1)."\n";
    } elsif (/^%\{error:(.+)}/) {
      die expandmacros($1)."\n";
    }

### Single-line %commands
    # Multi-level submodules
    elsif (/^%include\s+(.+)/) {
      parse_spec(expandmacros($1),$stage,$subname,$scriptlet);
    }

    # EXPERIMENTAL: %{perl:interpreter}
    elsif (/^%\{perl:(.+)}/) {
      my $perl = expandmacros('%{_tmppath}')."/deb-tmp.perl.".int(rand(99998)+1);
      do {
        local *STDOUT;
        if (open(STDOUT, '>', $perl)) {
          eval($1);
        }
      };
      parse_spec($perl,$stage,$subname,$scriptlet);
      unlink $perl;
    }

    # Print the active macro table
    elsif (/^%dump/) {
      for (sort keys %specglobals) {
        print "%$_ ==> $specglobals{$_}\n" unless $_ =~ /^[A-Z]+$/;
      }
    }

    # Preprocess %define's and Conditional Build Stuff
    elsif (/^%(?:(?:un)?define|global|bcond_with(?:out)?)\s/) {
      expandmacros($_);
    }

### Multi-line sectioning %commands
# Now we pick out the sections and set "state" to parse that section.
# Fugly but I can't see a better way.  >:(

    elsif (/^%(description|copyrightdata|files)(?:\s+(?:-n\s+)?(.+))?/) {
      $stage = $1;
      $subname = 'main';
      if ($2) {       # Magic to add entries to the right package
        my $tmp = expandmacros($2);
        $subname = /-n/ ? $tmp : "$pkgdata{main}{name}-$tmp";
      }
    } # %description, %copyrightdata, %files

    elsif (/^%(package)\s+(?:-n\s+)?(.+)/) {
      $stage = $1;
      # Magic to add entries to the right package
      my $tmp = expandmacros($2);
      $subname = /-n/ ? $tmp : "$pkgdata{main}{name}-$tmp";
      push @pkglist, $subname;
      # Hack the filename for the package into a Debian-tool-compatible format.  GRRRRRR!!!!!
      # Have I mentioned I hate Debian Policy?
      ($pkgdata{$subname}{name} = $subname) =~ tr/_/-/;
      $pkgdata{$subname}{version} = $pkgdata{main}{version};
  # Build "same arch as previous package found" by default.  Where rpm just picks the
  # *very* last one, we want to allow arch<native>+arch-all
  # (eg, Apache is i386, but apache-manual is all)
      $pkgdata{$subname}{arch} = $pkgdata{main}{arch};  # Since it's likely subpackages will NOT have a BuildArch line...
    } # %package

    elsif (/^%(prep)/) {
      $stage = $1;
    } # %prep

    elsif (/^%(build|install|check|clean)/) {
      $stage = $1;
      $script{$stage} .= "cd '%{buildsubdir}'\n" if $pkgdata{main}{hassetup};
    } # %build,install,check,clean

    elsif (/^%(pre|post|preun|postun)\b(?:\s+(?:-n\s+)?(.+))?/i) {
      $stage = 'prepost';
      $scriptlet = lc $1;
      $subname = 'main';
      if ($2) {       # Magic to add entries to the right package
        my $tmp = expandmacros($2);
        $subname = /-n/ ? $tmp : "$pkgdata{main}{name}-$tmp";
      }
    } # %pre/%post/%preun/%postun

    elsif (/^%(changelog)/) {
      $stage = $1;
      if ($pkgdata{main}{$stage}) { # Multi-part changelog
        $pkgdata{main}{$stage} =~ s/\s+$/\n\n/g; # Trim trailing blanks
      } else {
        $pkgdata{main}{$stage} = '';
      }
    }

### Actual section contents
# now we handle individual lines from the various sections

    elsif ($stage eq 'description') {
      $pkgdata{$subname}{$stage} .= " $_";
    } # description

    elsif ($stage eq 'copyrightdata') {
      $pkgdata{$subname}{$stage} .= $_;
    } # copyrightdata

    elsif ($stage eq 'package') {
      # gotta expand %defines here.  Whee.
# Note that we look for the Debian-specific Recommends, Suggests, and Replaces,
# although they will have to be wrapped in '%if %{_vendor} == "debbuild"' for
# an rpmbuild-compatible .spec file
      if (my ($dname,$dvalue) = /^(Summary|Group|Version|BuildArch(?:itecture)?|
          Recommends|Suggests|Enhances|Replaces|PreReq|
          Requires(?:\(pre(?:un)?\))?|Conflicts|Provides|Pre-Depends):\s+(.+)$/ix) {
        $dname =~ tr/[A-Z]/[a-z]/;
        if ($dname =~ /^BuildArch/i) {
          $dvalue =~ s/^noarch/all/ig;
          $dname = 'arch';
        }
        if (grep { /$dname/ } qw{prereq requires(pre) requires(preun)}) {
          push @{$pkgdata{$subname}{'pre-depends'}}, splitreqs($dvalue);
        } elsif (grep { /$dname/ } qw(recommends suggests enhances replaces
            requires conflicts provides pre-depends)) {
          push @{$pkgdata{$subname}{$dname}}, splitreqs($dvalue);
        } else {
          $pkgdata{$subname}{$dname} = expandmacros($dvalue);
        }
      }
    } # package

    elsif ($stage eq 'prep') {
      # Actual handling for %prep section.  May have %setup macro, may include
      # %patch tags, may be just a bare shell script.
      # %autosetup and %autopatch are supported, too.
      if (s/^%autosetup//) {
        process_autosetup($stage);
      } # fall through, most likely to '%autopatch'
      if (s/^%autopatch//) {
        process_autopatch($stage);
      } elsif (s/^%setup//) {
        $script{$stage} .= process_setup();
      } elsif (s/^%patch//) {
        $script{$stage} .= process_patch();
      } else {
        $script{$stage} .= $_;
      }
    } # prep

    elsif ($stage =~ /build|install|check|clean/) {
      $script{$stage} .= $_;
    } # build,install,check,clean

    elsif ($stage eq 'prepost') {
      $pkgdata{$subname}{$scriptlet} .= $_;
    } # prepost

    elsif ($stage eq 'files') {
      if (/^%defattr         # lot of formatting whitespace permitted
          \s*\(\s*           # opening and closing parentheses; required
            (-|\d+)          # (1) file mode, numeric (octal) or '-'
          [\s,]+             # field separator, comma or space
            (-|(['"]?)\w+\3) # (2) default owner, (3) quotes permitted, or '-'
          [\s,]+             # field separator, comma or space
            (-|(['"]?)\w+\5) # (4) default group, (5) quotes permitted, or '-'
            (?:[\s,]+        # field separator, comma or space
               (-|\d+))?     # (6) directory mode; optional, then eq file mode
          \s*\)/x) {
        ($defattr{filemode}, $defattr{owner}, $defattr{group}) = ($1, $2, $4);
         $defattr{dirmode} = ($6 or $defattr{filemode}); # default directory setting is optional
      } elsif (/^%exclude/) {
        warn "'%exclude' keyword found, ignoring input line\n";
      } else {
        process_filesline($subname);
      }
    } # files

    elsif ($stage eq 'changelog') {
      # this is one of the few places we do NOT generally want to replace macros...
      $pkgdata{main}{$stage} .= $_;
    }

    elsif ($stage eq 'preamble') {
      if (/^(summary|name|epoch|version|release|
             group|copyright|url|packager):\s*(.+)/ix) {
        $pkgdata{main}{lc $1} //= expandmacros($2);
        $pkgdata{main}{lc $1} =~ tr/_/-/ if 'name' eq $1;
      } elsif (/^(vendor|buildroot):\s*(.+)/i) {
        $specglobals{lc $1} = $2;
      } elsif (my ($srcnum, $src) = /^source(\d*):\s*(.+)/i) {
        $src =~ s/\s*$//;
        $srcnum ||= 0;
        $pkgdata{sources}{$srcnum} = basename($src);
        $pkgdata{main}{source} = $pkgdata{sources}{0} if 0 == $srcnum;
      } elsif (my ($patchnum, $patch) = /^(patch\d*):\s*(.+)/i) {
        $patch =~ s/\s*$//;
        $pkgdata{main}{lc $patchnum} = basename($patch);
      } elsif (/^buildarch(?:itecture)?:\s*(.+)/i) {
        ($pkgdata{main}{arch} = $1) =~ s/^noarch$/all/;
      } elsif (/^buildrequires:\s*(.+)/i) {
        push @buildreq, splitreqs($1);
      } elsif (/^(requires|provides|conflicts):\s*(.+)/i) {
        push @{$pkgdata{main}{lc $1}}, splitreqs($2);
      } elsif (/^(?:prereq|requires\((?:pre|preun)\)):\s*(.+)/i) {
        push @{$pkgdata{main}{'pre-depends'}}, splitreqs($1);
      } elsif (/^recommends:\s*(.+)/i) {
        push @{$pkgdata{main}{recommends}}, splitreqs($1);
        warn "Warning:  Debian-specific 'Recommends:' outside %if wrapper\n" unless @ifexpr;
# As of sometime between RHEL 6 and RHEL 7 or so, support was added for Recommends: and Enhances:,
# along with shiny new tag Supplements:.  We'll continue to warn about them for a while.
      } elsif (/^(suggests|enhances|replaces|pre-depends):\s*(.+)/i) {
        push @{$pkgdata{main}{lc $1}}, splitreqs($2);
        warn "Warning:  '$1:' outside %if wrapper\n" unless @ifexpr;
      } elsif (/^supplements:\s*(.+)/i) {
        push @{$pkgdata{main}{enhances}}, splitreqs($1);
        warn "Warning:  'Supplements:' is not natively supported by .deb packages.  Downgrading relationship to Enhances:.\n";
      } elsif (/^obsoletes:\s*(.+)/i) {
        push @{$pkgdata{main}{replaces}}, splitreqs($1);
      } elsif (/^autoreq(?:prov)?:\s*(.+)/i) {
	# we don't handle auto-provides (yet)
	$NoAutoReq = 1 if $1 =~ /(?:no|0)/i;
      } else { # Other lines may contain '%{?!contional:macros}' as well
        expandmacros($_);
      }
    } # preamble

  } # while <$fh>

  close $fh;

  die "Unmatched %if at end of file. Missing %endif.\n" if @ifexpr;
} # end parse_spec()


## splitreqs()
# Split string at 'comma' and return list of trimmed entries
sub splitreqs {
  my ($reqs) = @_;
  return map { /^\s*(.+)\s*$/ } split /,/, expandmacros($reqs);
} # end splitreqs()


## process_autosetup()
# Convert the current '$_' line from '%autosetup' to '%setup' and call
# 'process_setup()', then create an '%autopatch' line and fall through to
# 'process_autopatch()'.
sub process_autosetup {
  my ($stage) = @_;
  chomp; $_ = expandmacros($_); # permit '%{!?with_patches:-N}'
  my $plevel = $1 if s/\s+-p\s*(\d+)//;
  my $verbose = s/\s+-v//; $_ .= ($verbose ? '': ' -q');
  my $noautopatch = s/\s+-N//; # Eat '-N', which is unknown to %setup
  expandmacros("%global __scm $1") if s/\s+\-S\s+(\w+)//; # dito '-S'
  # Generic %setup invocation
  $script{$stage} .= process_setup(); # hmm, how to fall to '%setup'?
  # Do SCM stuff
  my $scm = expandmacros('%__scm');
  die "%autosetup: option '-S $scm' currently not supported at line $..\n"
    unless $specglobals{"__scm_setup_$scm"};
  $script{$stage} .= "%{__scm_setup_$scm".($verbose ? '' : ' -q')."}\n";
  $_ = $noautopatch ? '' :
    '%autopatch'.($plevel ? " -p $plevel" : '').($verbose ? ' -v' : '');
} # process_autosetup()


## process_autopatch()
# Apply all available patches in ascending numerical order as specified in the
# specfile. The '%autopatch' command only knows the options '-v' ('verbose) and
# '-p[N]' ('path strip level').
sub process_autopatch {
  my ($stage) = @_;
  my $verbose = /-v/;
  my ($plevel) = /-p\s*(\d+)/;
  my @patches = grep {/^patch/} keys %{$pkgdata{main}};
  if (1 == @patches) { # won't enter 'sort BLOCK' and strip 'patch' prefix
    my $patchfile = $pkgdata{main}{$patches[0]};
    $script{$stage} .= apply_patch($verbose,$plevel,$patchfile);
  } else { # now you're talkin', brother!
    for my $patch (sort {$a =~ s/patch//; $b =~ s/patch//; $a <=> $b} @patches) {
      my $patchfile = qq(%{_sourcedir}/$pkgdata{main}{"patch$patch"});
      $script{$stage} .= apply_patch($verbose,$plevel,$patchfile);
    }
  }
} # process_autopatch()


## apply_patch()
# Hard-coded variant of RPM's macro of the same name.
sub apply_patch {
  my ($verbose,$plevel,$patchfile) = @_;
  return expandmacros('%{apply_patch'.
    ($verbose ? '' : ' -q').($plevel ? " -p$plevel" : '').
    " -m %{basename:$patchfile} $patchfile}\n");
} # apply_patch()


## process_setup()
sub process_setup {
  $pkgdata{main}{hassetup} = 1;  # flag the fact that we've got %setup
  # Parse out the %setup macro.  rpmbuild doesn't complain about
  # gibberish immediately following %setup, but we will
  if (not /^(?:\s|$)/) {
    chomp;
    warn "Suspect %setup tag '%setup$_', continuing\n";
    s/^\S+//;
  }

  # Prepare some flags
  my ($createdir, $leavedirs, $quietunpack, $skipdefault) = (0) x 4;
  my (@sbefore, @safter);

  Getopt::Long::GetOptionsFromString($_,
    'n=s' => \$specglobals{buildsubdir},
    'c'   => \$createdir,   # flag, create and change directory before unpack
    'D'   => \$leavedirs,   # flag, do not delete directory before unpack
    'T'   => \$skipdefault, # flag, do not unpack first source
    'q'   => \$quietunpack, # SSH!  Unpack quietly
    'b=i' => \@sbefore,
    'a=i' => \@safter);

# Note that this is an incomplete match to rpmbuild's full %setup expression.
# Known differences
# - rpmbuild requires -n on all %setup macros, but carries the first down to
#   %install etc, debbuild sets the global on the first call, and keeps using
#   it for further %setup calls
  my $setupscript = "cd '%{_builddir}'\n";
  $setupscript .= "%{__rm} -rf '%{buildsubdir}'\n" unless $leavedirs;

  foreach (@sbefore) {
    $setupscript .= unpackcmd($pkgdata{sources}{$_},$quietunpack);
  }

  if ($createdir) {
    $setupscript .= "%{__mkdir_p} %{buildsubdir}\n";
    $setupscript .= "cd '%{buildsubdir}'\n";
  }
  elsif (not $skipdefault) {
    $setupscript .= unpackcmd($pkgdata{main}{source},$quietunpack);
  }

  if (not $createdir) {
    $setupscript .= "cd '%{buildsubdir}'\n";
  }
  elsif (not $skipdefault) {
    $setupscript .= unpackcmd($pkgdata{main}{source},$quietunpack);
  }

  foreach (@safter) {
    $setupscript .= unpackcmd($pkgdata{sources}{$_},$quietunpack);
  }

# rpm doesn't seem to do the chowns any more
#		qq([ `%{__id_u}` = '0' ] && %{__chown} -Rhf root .\n).
#		qq([ `%{__id_u}` = '0' ] && %{__chgrp} -Rhf root .\n).
  return $setupscript .= qq(%{__chmod} -Rf a+rX,u+w,go-w .\n);
} # end process_setup()


## unpackcmd()
# Prepare the necessary commands for uncompressing and extracting the content
# of the source drop according to the file extension.
sub unpackcmd {
   my ($sourcedrop,$quietunpack) = @_;
   return (
      $sourcedrop =~ /\.zip$/ ? # .zip files are not really tarballs
         '%{__unzip}'.( $quietunpack ? ' -qq ' : ' ' ).
         "'%{_sourcedir}/$sourcedrop'" :
      $sourcedrop =~ /\.tar$/ ? # plain .tar files don't need to be uncompressed
         '%{__tar} -x'.( $quietunpack ? '' : 'vv' ).'f '.
         "'%{_sourcedir}/$sourcedrop'" :
      decompress("%{_sourcedir}/$sourcedrop").
          ' | %{__tar} -x'.( $quietunpack ? '' : 'vv' ).'f -' ).
      qq(\nSTATUS=\$?\nif [ \$STATUS -ne 0 ]; then\n  exit \$STATUS\nfi\n);
} # end unpackcmd()


## decompress()
# Determine the suitable decompressor according to the file extension.
sub decompress {
   my ($filename) = @_;
   return (
      $filename =~ /\.(?:tgz|(?:gz|Z))$/ ? '%{__gzip}'  :
      $filename =~ /\.bz2$/              ? '%{__bzip2}' :
      $filename =~ /\.xz$/               ? '%{__xz}'    :
      die("Can't handle unknown file type '$filename'.\n") ).
      " -dc '$filename'";
} # end decompress()


## lookup_specfile()
# Used for '-t[pcilabs]'.
sub lookup_specfile {
  my ($tarball) = @_;
  return (
     $tarball =~ /\.zip$/ ? # .zip files are not really tarballs
        q(%{__unzip} -Z1 %{tarball} '*.spec') :
     $tarball =~ /\.tar$/ ? # plain .tar files don't need to be uncompressed
        q(%{__tar} -tf %{tarball} --wildcards '*.spec') :
     decompress($tarball).q( | %{__tar} -tf - --wildcards '*.spec') );
}
# end lookup_specfile()


## extract_specfile()
# Used for '-t[pcilabs]'.
sub extract_specfile {
  my ($tarball) = @_;
  return (
     $tarball =~ /\.zip$/ ? # .zip files are not really tarballs
        q(%{__unzip} -p %{tarball} '*.spec') :
     $tarball =~ /\.tar$/ ? # plain .tar files don't need to be uncompressed
        q(%{__tar} -xOf %{tarball} --wildcards '*.spec') :
     decompress($tarball).q( | %{__tar} -xOf - --wildcards '*.spec') ).
     qq( > %{specfile}\n%{__cp} -f %{tarball} %{_sourcedir});
}
# end extract_specfile()


## process_patch()
sub process_patch {
  # Things rpmbuild Does
  # -> blindly follows Patch(.*):  ==>  %patch$1
  # %patch0 does not in fact equal %patch without -P
  # spaces optional between flag and argument
  # multiple -P options actually trigger multiple patch events.  >_<
  # can we emulate this?
  # yes we can!
  my @patchlist;

  # add patch{nn} to the list
  if (s/^(\d+)//) {
    push @patchlist, $1;
  }
  # add the "null" patch to the list unless we've got a -P flag
  elsif (not /-P/) {
    push @patchlist, '';
  }

  # %patch options:
  my ($fuzz, $plev) = ($specglobals{_default_patch_fuzz}, 0);
  my ($psuff, $noempty, $reverse, $altdir, $output) = ('') x 5;

  Getopt::Long::GetOptionsFromString($_,
    'P=i'   => \@patchlist, # patch number(s)
    'p=i'   => \$plev,      # path strip.  Passed to patch as-is
    'F=i'   => \$fuzz,      # fuzz factor.  Passed to patch as-is
    'd=s'   => \$altdir,    # alternative directory.  Passed to patch as-is
    'o=s'   => \$output,    # redirect output.  Passed to patch as-is
    'E'     => \$noempty,   # remove empty files.  Passed to patch as-is
    'R'     => \$reverse,   # reverse patch.  Passed to patch as-is
    'b|z=s' => \$psuff,     # backup file postfix.
      # Literal, if e.g. "bkfile", backup files will be "filebkfile",
      # not "file.bkfile".  Passed as-is, with a minor flag adjustment
    '<>' => sub {
      push @patchlist, @_; # all other arguments are patch numbers
    }
  );

  my $patchopts = $specglobals{_default_patch_flags};
  $patchopts .= " --fuzz=$fuzz -p$plev";
  $patchopts .= " -b --suffix=$psuff" if $psuff;
  $patchopts .= " -d $altdir" if $altdir;
  $patchopts .= " -o $output" if $output;
  $patchopts .= ' -E' if $noempty;
  $patchopts .= ' -R' if $reverse;

  my $patchscript;
  foreach my $pnum (@patchlist) {
    $patchscript .= q(echo "Patch ).($pnum eq '' ? '' : "#$pnum ");
    $pnum = expandmacros("%{patch$pnum}");
    $patchscript .= q|(|.basename($pnum).qq|):"\n|;
    $patchscript .= qq(%{uncompress:$pnum} | %{__patch} $patchopts\n\n);
  }
  return $patchscript;
} # end process_patch()


## uncompress()
# Prepare the necessary commands for uncompressing the content of a patch file
# for piping to 'patch' in the next step according to the file extension.
sub uncompress {
  my ($patchfile) = @_;
  return (
    # Compressed patch.  You weirdo.
    $patchfile =~ /\.(?:Z|gz|bz2|xz)$/ ? decompress($patchfile) :
    # .zip'ed patch.  *backs away slowly*
    $patchfile =~ /\.zip$/ ? "%{__unzip} -p $patchfile" :
    # else uncompressed patch
    "%{__cat} $patchfile" );
} # uncompress()


## process_filesline()
sub process_filesline {
  my ($subname) = @_;

  # create and initialize flags
  my ($perms, $owner, $group, $conf) =
    ($defattr{filemode}, $defattr{owner}, $defattr{group}, '-');

  # Debian dpkg doesn't speak "%docdir".  Meh.
  my $dir_only = s/^%(?:dir|docdir)\s*//;

  # strip and flag %attr constructs ... and wipe it when we're done.
  if (s/^%attr           # lot of formatting whitespace permitted
      \s*\(\s*           # opening and closing parentheses; required
        (-|\d+)          # (1) file mode, numeric (octal) or '-'
      [\s,]+             # field separator, comma or space
        (-|(['"]?)\w+\3) # (2) default owner, (3) quotes permitted, or '-'
      [\s,]+             # field separator, comma or space
        (-|(['"]?)\w+\5) # (4) default group, (5) quotes permitted, or '-'
      \s*\)\s*//x) {
    ($perms,$owner,$group) = ($1,$2,$4);
  }

  # Conffiles.  Note that Debian and RH have similar, but not
  # *quite* identical ideas of what constitutes a conffile.  Nrgh.
  # Note that dpkg will always ask if you want to replace the file - noreplace
  # is more or less permanently enabled.
##fixme
# also need to handle missingok (file that doesn't exist, but should be removed on uninstall)
# hmm.  not sure if such is **POSSIBLE** with Debian...  maybe an addition to %post?
  if (s/%config\b(?:\s*\(\s*noreplace\s*\)\s*)?//) {
    $pkgdata{$subname}{conffiles} = 1;  # Flag it for later
    $conf = 'y';
  }

  # %doc needs extra processing, because it can be a space-separated list, and may
  # include both full and partial pathnames.  The partial pathnames must be fiddled
  # into place in the %install script, because Debian doesn't really have the concept
  # of "documentation file" that rpm does.  (Debian "documentation files" are files
  # in /usr/share/doc/<packagename>.)
  if (s/%doc\s+//) {
    # have to extract the partial pathnames that %doc installs automagically
    foreach my $pp (split) {
      if (not $pp =~ m|^[%/]|) {
        $doclist{$subname} .= " $pp";
        my ($element) = $pp =~ m|([^/\s]+/?)$|;
        s|$pp|%{_docdir}/$pkgdata{$subname}{name}/$element|;
      }
    }
  } # $filesline =~ /%doc\b/

  s/^\s*//; chomp;	# Just In Case.  For, uh, neatness.

# due to Debian's total lack of real permissions-processing in its actual package
# handling component (dpkg-deb), this can't really be done "properly".  We'll have
# to add chown/chmod commands to the postinst instead.  Feh.
  $pkgdata{$subname}{post} .= "%{__chown} -Rh $owner $_\n" if $owner ne '-';
  $pkgdata{$subname}{post} .= "%{__chgrp} -Rh $group $_\n" if $group ne '-';
  if (/\*/) { # should actually 'glob'; will assume type 'file'
    $pkgdata{$subname}{post} .= "%{__chmod} $perms $_\n" if $perms ne '-';
  } else { # single entry; either 'directory' or 'file'
    $pkgdata{$subname}{post} .= "if [ -d $_ ]; then %{__chmod} $defattr{dirmode} $_; fi\n" if $defattr{dirmode} ne '-';
    $pkgdata{$subname}{post} .= "if [ -f $_ ]; then %{__chmod} $perms $_; fi\n" if $perms ne '-';
  }

##fixme
# need hackery to assure only one filespec per %config.  NB:  "*" is one filespec.  <g>
  push @{$pkgdata{$subname}{conflist}}, $_ if $conf ne '-';

  # now that we've got the specials out of the way, we can add things to the appropriate list of files.
  # ... and finally everything else
  $filelist{$subname} .= " $_" unless $dir_only;
} # end process_filesline()


## execute_script()
# Writes and executes a %script (mostly) built while reading the spec file.
sub execute_script {
  my ($stage, $what, $for) = @_;

  # anything to do?
  return if $specglobals{uc "no$stage"} or not $script{$stage};

  # create script filename
  my $scriptfile = expandmacros('%{_tmppath}')."/deb-tmp.$stage.".int(rand(99998)+1);
  sysopen(SCRIPT, $scriptfile, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW, 0777)
    or die "Can't open/create ".($what or $stage)."script file $scriptfile: $!\n";
  print SCRIPT expandmacros('%{___build_template}');
  print SCRIPT expandmacros($script{$stage});
  close SCRIPT;

  # execute
  print 'Calling '.($what or "%$stage")." script $scriptfile".($for or '')."...\n";
  system($scriptfile) and die "Can't exec: $!\n";

  # and clean up
  unlink $scriptfile;
} # end execute_script()


## install()
# Writes and executes the %install script (mostly) built while reading the spec file.
sub install {

  # munge %doc entries into place
  # rpm handles this with a separate executed %doc script, we're not going to bother.
  foreach my $docpkg (keys %doclist) {
    $script{install} .= "DOCDIR=\$RPM_BUILD_ROOT%{_docdir}/$pkgdata{$docpkg}{name}\nexport DOCDIR\n";
    $script{install} .= "%{__mkdir_p} \$DOCDIR\n";
    $doclist{$docpkg} =~ s/^\s*//;
    foreach (split ' ', $doclist{$docpkg}) {
      $script{install} .= "%{__cp} -pr $_ \$DOCDIR/\n";
    }
  }

  execute_script('install');

  # final bit: compress manpages if present
  # done here cuz I don't grok shell well
  # should probably error-check all kinds of things.  <g>
  foreach my $manpage (glob("$specglobals{buildroot}/usr/share/man/man*/*")) {
    if ( -f $manpage) {
      if (my ($newpage) = $manpage =~ /^(.+)\.(?:Z|gz|bz2)\n?$/) {
       qx($specglobals{__gzip} -d $manpage) if $manpage =~ /\.(?:Z|gz)$/;
       qx($specglobals{__bzip2} -d $manpage) if $manpage =~ /\.bz2$/;
       $manpage = $newpage;
      }
      qx($specglobals{__gzip} -f9n $manpage);
    } elsif ( -l $manpage) {
      (my $linkdest = readlink $manpage) =~ s/\.(?:Z|gz|bz2)//;
      unlink $manpage;
      $manpage =~ s/\.(?:Z|gz|bz2)//;
      symlink "$linkdest.gz", "$manpage.gz" or print "DEBUG: wibble: symlinking manpage failed: $!\n";
    }
  }

  execute_script('check');
} # end install()


## binpackage()
# Creates the binary .deb package from the installed tree in $specglobals{buildroot}.
# Writes and executes a shell script to do so.
# Creates miscellaneous files required by dpkg-deb to actually build the package file.
# Should handle simple subpackages
sub binpackage {

  foreach my $pkg (@pkglist) {

    # Just In Case.
    $pkgdata{$pkg}{arch} //= $pkgdata{main}{arch};
    $pkgdata{$pkg}{copyrightdata} //= $pkgdata{main}{copyrightdata};
    $pkgdata{$pkg}{changelog} //= $pkgdata{main}{changelog};
    $pkgdata{$pkg}{group} //= $pkgdata{main}{group};

    # Make sure we have somewhere to write the .deb file
    mkdir "$topdir/DEBS/$pkgdata{$pkg}{arch}" unless -e "$topdir/DEBS/$pkgdata{$pkg}{arch}";

    # Skip building a package that doesn't have any files or dependencies.  True
    # metapackages don't have any files, but they depend on a bunch of things.
    # Packages with neither have, essentially, no content.
    next if
        (not $filelist{$pkg} or $filelist{$pkg} =~ /^\s*$/) &&
        (not $pkgdata{$pkg}{requires});
    $filelist{$pkg} //= '';

    # Gotta do this first, otherwise we don't have a place to move files from %files
    mkdir "$specglobals{buildroot}/$pkg";

    # Eliminate any lingering % macros
    $filelist{$pkg} = expandmacros($filelist{$pkg});

    foreach my $pkgfile (split ' ', $filelist{$pkg}) {
      # Feh.  Manpages don't **NEED** to be gzipped, but rpmbuild does, and so shall we.
      # ... and your little info page too!
      if ($pkgfile =~ m{/usr/share/(?:man/man|info)}) {
	# need to check to see if manpage is gzipped
	if (-e "$specglobals{buildroot}$pkgfile") {
	  # if we've just been pointed to a manpage section with "many" pages,
	  # we need to gzip them all.
	  # fortunately, we do NOT need to explicitly track each file for the
	  # purpose of stuffing them in the package...  the original %files
	  # entry will do just fine.
	  if ( -d "$specglobals{buildroot}$pkgfile") {
	    foreach my $globfile (glob("$specglobals{buildroot}$pkgfile/*")) {
	      qx ( $specglobals{__gzip} $globfile ) if $globfile !~ m|\.gz$|;
	    }
	  } else {
	    if ($pkgfile !~ m|\.gz$|) {
	      qx ( $specglobals{__gzip} $specglobals{buildroot}$pkgfile );
	      $pkgfile .= '.gz';
	    }
	  }
	} else {
	  if ($pkgfile !~ m|\.gz$|) {
	    $pkgfile .= '.gz' unless $pkgfile =~ /\*$/;
	  } else {
	    $pkgfile =~ s/\.gz$//;
	    qx ( $specglobals{__gzip} $specglobals{buildroot}$pkgfile );
	    $pkgfile .= '.gz';
	  }
	}
      }

      my ($fpath,$fname) = $pkgfile =~ m|(.+?/?)?([^/]+/?)$|;	# We don't need $fname now, but we might.
      qx ( $specglobals{__mkdir_p} $specglobals{buildroot}/$pkg$fpath ) if $fpath;
      qx ( $specglobals{__cp} -ar $specglobals{buildroot}$pkgfile $specglobals{buildroot}/$pkg$fpath );
    }

    # EXPERIMENTAL: Add 'changelog' and 'copyrightdata' sections as 'doc' files.
    if ($pkgdata{$pkg}{changelog}) {
      $pkgdata{$pkg}{changelog} =~ s/\s+$//g; # Trim trailing blanks
      my $clpath = expandmacros("%{buildroot}/$pkg%{_docdir}/$pkgdata{$pkg}{name}");
      qx ( $specglobals{__mkdir_p} $clpath );
      if (open CHANGELOG, "| $specglobals{__gzip} -cf9n >$clpath/changelog.gz") {
        print CHANGELOG $pkgdata{$pkg}{changelog};
        close CHANGELOG;
      }
    }
    if ($pkgdata{$pkg}{copyrightdata}) {
      $pkgdata{$pkg}{copyrightdata} =~ s/\s+$//g; # Trim trailing blanks
      my $crpath = expandmacros("%{buildroot}/$pkg%{_docdir}/$pkgdata{$pkg}{name}");
      qx ( $specglobals{__mkdir_p} $crpath );
      if (open COPYRIGHT, "| $specglobals{__gzip} -cf9n >$crpath/copyright.gz") {
        print COPYRIGHT $pkgdata{$pkg}{copyrightdata};
        close COPYRIGHT;
      }
    }

    # Get the "Depends" (Requires) a la RPM.  Ish.  In case there were
    # "Requires" specified in the spec file, those would precede these.
    push @{$pkgdata{$pkg}{requires}}, getreqs("$specglobals{buildroot}/$pkg") unless $NoAutoReq;

    # Gotta do this next, otherwise the control file has nowhere to go.  >:(
    mkdir "$specglobals{buildroot}/$pkg/DEBIAN";

    # Munge things so that Debian tools don't choke on errant blank lines
    $pkgdata{$pkg}{description} =~ s/\s+$//g;	# Trim trailing blanks
    $pkgdata{$pkg}{description} =~ s/^ $/ ./mg;	# Replace lines consisting of " \n" with " .\n"

    # Give an estimate of the installation size
    my ($installedsize) =
      qx($specglobals{__du} -s --apparent-size $specglobals{buildroot}/$pkg) =~
        /(\d+)/;

    my $control = "Package: $pkgdata{$pkg}{name}\n".
	'Version: '.format_version($pkg)."\n".
	( defined $pkgdata{$pkg}{group} ? "Section: $pkgdata{$pkg}{group}\n" : '' ).
	"Priority: optional\n".
	"Architecture: $pkgdata{$pkg}{arch}\n".
	"Installed-Size: $installedsize\n".
	"Maintainer: %{packager}\n".
	"Description: $pkgdata{$pkg}{summary}\n$pkgdata{$pkg}{description}\n".
	( defined $pkgdata{main}{url} ? "Homepage: %{url}\n" : '' );
    foreach my $deplist (qw(recommends suggests enhances replaces
                            requires conflicts provides pre-depends)) {
      if (defined $pkgdata{$pkg}{$deplist} and @{$pkgdata{$pkg}{$deplist}}) {
        my $tag = $deplist eq 'requires' ? 'depends' : $deplist;
        $tag =~ s/-depends/-Depends/;
        $control .= "\u$tag: ".join(',', do {
          my %seen; grep { !$seen{$_}++ } # uniq
          map { my ($name,$rel,$ver) = splitver($_);
            # magic needed to properly version dependencies...
            $ver eq '0' ? $name : "$name ($rel $ver)" }
              @{$pkgdata{$pkg}{$deplist}} })."\n";
      }
    }

    open CONTROL, ">$specglobals{buildroot}/$pkg/DEBIAN/control" or die;
    print CONTROL expandmacros($control);
    close CONTROL;

    # Iff there are conffiles (as specified in the %files list(s), add'em
    # in so dpkg-deb can tag them.
    if ($pkgdata{$pkg}{conffiles}) {
      open CONFLIST, ">$specglobals{buildroot}/$pkg/DEBIAN/conffiles" or die;
      foreach my $conffile (@{$pkgdata{$pkg}{conflist}}) {
	$conffile = expandmacros($conffile);
	foreach (glob "$specglobals{buildroot}/$pkg/$conffile") {
	  s|$specglobals{buildroot}/$pkg/||g;	# nrgl.  gotta be a better way to do this...
	  s/\s+/\n/g;	# Not gonna support spaces in filenames.  Ewww.
	  print CONFLIST "$_\n";
	}
      }
      close CONFLIST;
    }

    # found the point of scripts on subpackages.
    foreach my $scr (qw(pre post preun postun)) {
      my $scrfile = $scr;
      $scrfile .= 'inst' unless $scrfile =~ s/un/rm/;
      if ($pkgdata{$pkg}{$scr}) {
        open SCRIPT, ">$specglobals{buildroot}/$pkg/DEBIAN/$scrfile" or die;
        print SCRIPT expandmacros("#!%{___build_shell} %{___build_args}\n");
        print SCRIPT expandmacros($pkgdata{$pkg}{$scr});
        close SCRIPT;
        chmod 0755, "$specglobals{buildroot}/$pkg/DEBIAN/$scrfile";
      }
    }

    $script{pkg} = "%{__fakeroot} -- %{__dpkg_deb} -b %{buildroot}/$pkg ".
        "%{_debdir}/$pkgdata{$pkg}{arch}/".format_debfile($pkg)."\n";

    execute_script('pkg', 'package-creation', " for $pkgdata{$pkg}{name}");

    $finalmessages .= 'Wrote binary package '.format_debfile($pkg).
	" in $topdir/DEBS/$pkgdata{$pkg}{arch}\n";
  } # subpackage loop

} # end binpackage()


## format_version()
# Glue together epoch/version/release in a common format
sub format_version {
  my ($pkg) = @_;
  return (defined $pkgdata{main}{epoch} ? "$pkgdata{main}{epoch}:" : '').
    "$pkgdata{$pkg}{version}-$pkgdata{main}{release}";
} # end format_version()


## format_debfile()
# %$&$%@#@@#%@@@ Debian and their horrible ugly package names.  >:(
sub format_debfile {
  my ($pkg) = @_;
  return "$pkgdata{$pkg}{name}_".format_version($pkg)."_$pkgdata{$pkg}{arch}.deb";
} # format_debfile()


## format_sdebfile()
sub format_sdebfile {
  return "$pkgdata{main}{name}-".format_version('main').'.sdeb';
} # format_sdebfile()


## splitver()
# Split un/versioned requirement
sub splitver {
  my ($req) = @_;
  # from rpmbuild error message
  # Dependency tokens must begin with alpha-numeric, '_' or '/'
##fixme:  check for suitable whitespace around $rel
  # We have two classes of requirements - versioned and unversioned.
  $req .= ' >= 0' unless $req =~ /[><=]/; # unversioned buildreq
  # Hack up the perl(Class::SubClass) deps into something dpkg can understand.
  # May or may not be versioned.
  # We do this first so the version rewriter can do its magic next.
  if (my ($mod,$ver) = $req =~ /^perl\(([\w:+-]+)\)\s*([><=]+.+)?/) {
    ($req = lc "lib$mod-perl") =~ s/::/-/g;
     $req .= $ver if $ver;
  }
  # Pick up the details of versioned buildreqs
  return $req =~ /([\w.+-]+)\s*([><=]+)\s*([\w:.~+-]+)/;
} # end splitver()


## srcpackage()
# Builds a .src.deb source package.  Note that Debian's idea of
# a "source package" is seriously flawed IMO, because you can't
# easily copy it as-is.
# Not quite identical to RPM, but Good Enough (TM).
sub srcpackage {
  my $pkgsrcname = format_sdebfile();

  # We'll definitely need this later, and *may* need it sooner.
  my $barespec = basename($specglobals{specfile});

  my $paxcmd;

  # Copy the specfile to the build tree, but only if it's not there already.
  if (abs_path($specglobals{specfile}) ne abs_path("$topdir/SPECS/$barespec")) {
    $paxcmd .= "%{__cp} %{specfile} %{_specdir};\n"
  }

  # use pax -w [file] [file] ... -f outfile.sdeb
  $paxcmd .= '(cd %{_topdir}; %{__pax} -L -w ';

  # some packages may not have a "main" source.
  $paxcmd .= "SOURCES/$pkgdata{main}{source} " if $pkgdata{main}{source};

  # create file list:  Source[nn], Patch[nn]
  foreach my $source (sort keys %{$pkgdata{sources}}) {
    # Skip Source0, since it's also $pkgdata{main}{source}.  Could arguably
    # just remove that instead, but I think that might backfire.
    $paxcmd .= "SOURCES/$pkgdata{sources}{$source} " if $source != 0;
  }
  my @patches = grep {/^patch/} keys %{$pkgdata{main}};
  if (1 == @patches) { # won't enter 'sort BLOCK' and strip 'patch' prefix
    $paxcmd .= qq(SOURCES/$pkgdata{main}{$patches[0]});
  } else { # now you're talkin', brother!
    $paxcmd .= join(' ', map { qq(SOURCES/$pkgdata{main}{"patch$_"}) }
      sort { $a =~ s/patch//; $b =~ s/patch//; $a <=> $b } @patches);
  }

  # add the spec file and write to source package destination.
  $paxcmd .= " SPECS/$barespec -f %{_srcdebdir}/$pkgsrcname)";

  system(expandmacros($paxcmd)) == 0 and
  $finalmessages .= "Wrote source package $pkgsrcname in $topdir/SDEBS.\n";
} # end srcpackage()


## checkbuildreq()
# Checks the build requirements (if any)
# Spits out a rude warning and returns a true-false error if any
# requirements are not met.
sub checkbuildreq {
  return 1 unless @buildreq; # No use doing extra work.

  # expand macros
  my @reqlist = map { expandmacros($_) } @buildreq;

  if ( not -e $specglobals{__dpkg_query} ) {
    warn "**WARNING**  $specglobals{__dpkg_query} not found.  Can't check build-deps.\n".
	"  Required for sucessful build:\n".join(', ', @reqlist)."\n".
	"  Continuing anyway.\n";
    return 1;
  }

  my @missinglist;
  foreach my $req (@reqlist) {
    my ($pkg,$rel,$ver) = splitver($req);

## Apparently a package that has been installed, then uninstalled, has a "known" dpkg status of
## "unknown ok not-installed" vs a package that has never been installed which returns nothing.  O_o
## Virtual packages, of course, *also* show as "not installed" like this (WTF?)
## This causes real packages to be misdetected as installed "possible virtual packages" instead of "missing
## packages".  I don't think there's really a solution until/unless Debian's tools properly register virtual
## packages as installed.
    my ($pkgdep) = qx ( $specglobals{__dpkg_query} --showformat '\${status}\t\${version}\n' -W $pkg );
    if (not $pkgdep) {
      warn " * Missing build-dependency $pkg!\n";
      push @missinglist, $pkg;
    } else {
# real package, installed
#kdeugau:~$ dpkg-query --showformat '${status}\t${version}\n' -W libc-client2007e-dev 2>&1
#install ok installed    8:2007f~dfsg-1
# virtual package, provided by ???
#kdeugau:~$ dpkg-query --showformat '${status}\t${version}\n' -W libc-client-dev 2>&1
#unknown ok not-installed
# real package or virtual package not installed or provided
#kdeugau:~$ dpkg-query --showformat '${status}\t${version}\n' -W libdb4.8-dbg 2>&1
#dpkg-query: no packages found matching libdb4.8-dbg

      my ($reqstat,undef,undef,$reqver) = split ' ', $pkgdep;
      if ($reqstat =~ /^unknown/) {
	# this seems to be a virtual package.
	warn " * Warning: $pkg is probably installed but seems to be a virtual package.\n";
      } elsif ($reqstat =~ /^install/) {
	my ($resp) = qx ( $specglobals{__dpkg} --compare-versions $reqver '$rel' $ver && $specglobals{__echo} "ok" );
	if ($resp !~ /^ok/) {
	  warn " * Buildreq $pkg is installed, but wrong version ($reqver):  Need $ver\n"
        }
      } else {
	# whatever state it's in, it's not completely installed, therefore it's missing.
	warn " * Missing build-dependency $pkg!\n  $pkgdep";
        push @missinglist, $pkg;
      } # end not installed/installed check
    }
  } # end req loop

  print q(To install all missing dependencies, run 'apt-get install ).
    join(' ', @missinglist)."'.\n" if @missinglist;

  return 0 == @missinglist;
} # end checkbuildreq()


## getreqs()
# Find out which libraries/packages are required for any
# executables and libs in a given file tree.
# (Debian doesn't have soname-level deps;  just package-level)
# Returns an empty list if the tree contains no binaries.
# Doesn't work well on shell scripts. but those *should* be
# fine anyway.  (Yeah, right...)
sub getreqs {
  my ($pkgtree) = @_;

  print "Checking library requirements...\n";
  return () if 'all' eq $pkgdata{main}{arch}; # noarch package
  chomp( my @binlist = qx ( $specglobals{__find} $pkgtree -type f -perm 755 ) );
  return () unless @binlist;

  my $binlist = join(' ', @binlist);
  my @reqlist = grep { ! m|^/| } qx ( LANG=C $specglobals{__ldd} $binlist );

  # Get the list of libs provided by this package.  Still doesn't
  # handle the case where the lib gets stuffed into a subpackage.  :/
  my @intprovlist = qx ( $specglobals{__find} $pkgtree -type f -name "*.so*" -printf "%P\n" );

  my $reqliblist;
  foreach (@reqlist) {
    next if /not a dynamic executable/;
    next if m|/lib(?:64)?/ld-linux|;	# Hack! Hack!  PTHBTT!  (libc suxx0rz)
    next if /linux-(gate|vdso).so/;	# Kernel hackery for teh W1n!!1!1eleventy-one!1  (Don't ask.  Feh.)

    # Whee!  more hackery to detect provided-here libs.  Some apparently return from ldd as "not found".
    my ($a,$b) = split / => /;
    $a =~ s/\s//g;
    if ($b =~ /not found/) {
      next if qx ( $specglobals{__find} $specglobals{buildroot} -name "*$a" );
    }

    my ($req) = m|=\>\s+([\w./+-]+)|; # dig out the actual library (so)name.
	# And feh, we need the *path*, since I've discovered a new edge case where
	# the same libnnn.1.2.3 *file*name is found across *several* lib dirs.  >:(

    # Ignore libs provided by this package.  Note that we don't match
    # on word-boundary at the *end* of the lib we're looking for, as the
    # looked-for lib may not have the full soname version. (ie, it may
    # "just" point to one of the symlinks that get created somewhere.)
    next if grep { /\b$req/ } @intprovlist;

    $reqliblist .= " $req";
  }

# For now, we're done.  We're not going to meddle with versions yet.
# Among other things, it's messier than handling "simple" yes/no "do
# we have this lib?" deps.  >:(

  return unless $reqliblist; # possibly empty
  return map { m/^([\w+-]+?):/ } qx($specglobals{__dpkg_query} -S$reqliblist);

} # end getreqs()


## install_sdeb()
# Extracts .sdeb contents to %_topdir as appropriate
sub install_sdeb {
  my ($srcpkg) = @_;
  die "Can't install $srcpkg\n" unless $srcpkg =~ /\.sdeb$/;
  $srcpkg = abs_path($srcpkg);
  system(expandmacros("(cd %{_topdir}; %{__pax} -r -f $srcpkg)")) == 0 and
  print "Extracted source package $srcpkg to $topdir.\n";
} # end install_sdeb()


## store_value()
# Helper function for writing to the %macro storage(s)
sub store_value {
  my ($key,$value) = @_;
  if ($key =~ /^(?:summary|name|epoch|version|release|
      group|copyright|url|packager)$/ix) {
    $pkgdata{main}{$key} = (defined $value ? expandmacros($value) : undef);
  } else {
    $specglobals{$key} = (defined $value ? expandmacros($value) : undef);
  }
} # end store_value()


## expandmacros()
# Expands all %{blah} macros in the passed string
sub expandmacros {
  local ($_) = @_;
  return $_ unless /%/; # nothing to substitute

  goto CONDMAC if m/^%\{[?!]+/; # OMG: Conditional macros everywhere!

  # Process %define's and Conditional Build Stuff
  s/%(bcond_with(?:out)?)\s+(\S+)/%{$1 $2}/g;
  # Store %globals (from '%bcond_with[out]()' in 'macros[.in]')
  s/%(?:define|global)\s+(\S+)\s+(.+)/store_value($1,$2)/eg;
  # Permit unsetting/deleting of stored values
  { no warnings; s/%undefine\s+(\S+)/store_value($1)/eg; }

  # Replace global macros
  if (my ($macro) = m/^%(\w.*)$/) {
    s/$macro/{$macro}/;
  }
  if (my ($macro) = m/%(\w+)/) {
    s/%$macro/%{$macro}/g if grep { /$macro/ } qw(optflags getconfdir)
      or defined $specglobals{$macro}
      or defined $pkgdata{main}{$macro};
  }
  s/%\{optflags}/$optflags{$pkgdata{main}{arch}}/g if $pkgdata{main}{arch};
  s|%\{getconfdir}|$specglobals{_libdir}/debbuild|g;
  s/%\{(__scm_setup\w+?)}/handle_macro_options("%$1")/eg;

  # Package data
  s/%\{S:0}/%{source0}/g;
  s|%\{source0?}|%{_sourcedir}/$pkgdata{main}{source}|gi;
  foreach my $source (keys %{$pkgdata{sources}}) {
    s/%\{S:$source}/%{source$source}/g;
    s|%\{source$source}|%{_sourcedir}/$pkgdata{sources}{$source}|gi;
  }
  s/%\{P:(\d+)}/%{patch$1}/g;
  s|%\{(patch\d*)}|%{_sourcedir}/$pkgdata{main}{$1}|gi;

  # Globals, and not-so-globals
  # special %define's.  Handle the general case where we eval anything.
  # Even more general:  %(...) is a spec-parse-time shell code wrapper.
  # Prime example from 'macros.perl':
  # %define perl_vendorlib %(eval "`perl -V:installvendorlib`"; echo $installvendorlib)
    # Oy vey this gets silly for the perl bits.  Executing a shell to
    # call Perl to get the vendorlib/sitelib/whatever "core" globals.
    # This can do more, but...  eww.
  s/%\((.+)\)/my $in = expandmacros($1); chomp(my $res = qx($in)); $res/eg;
    # Yay!  ' characters apparently get properly exscapededed.

  simplemacros();

  # handle '%{parametrized macros}' with nested curly braces, all in one go!
  while (my ($prefix,$spc,$arg) = map { /{(\w+)(\s+)(.+)}/ } bracelet() ) {
    (my $initvalue = "%{$prefix$spc$arg}") =~ s/([{}])/\\$1/g; # neutralize curlies
    s/$initvalue/handle_macro_options("%$prefix $arg")/eg;
  }
  pos = 0; # reset to start of $_

  # handle '%{prefixed:macros}' with nested curly braces, all in one go!
  s/url2path/u2p/g;
  while (my ($prefix,$arg) = map { /{(\w+):(.+)}/ } bracelet() ) {
    no strict qw(refs); # we use strings as function refs
    (my $initvalue = "%{$prefix:$arg}") =~ s/([?!{}])/\\$1/g; # neutralize specials
    if ($prefix =~ /u2p/) {
      # curious form of 'basename'? - strip domain host (and port)
      (my $path = expandmacros($arg)) =~ s|\w+://[\w.:-]+||;
      s/$initvalue/$path/g;
    } elsif ($prefix =~ /basename|dirname/) {
      s/$initvalue/&$prefix(expandmacros($arg))/eg;
    } elsif ($prefix =~ /suffix/) {
      my (undef,undef,$suffix) = fileparse(expandmacros($arg),qr/\.[^.]*/);
      s/$initvalue/$suffix/g;
    } elsif ($prefix =~ /uncompress/) {
      s/$initvalue/uncompress($arg)/eg;
    } elsif ($prefix =~ /getenv/) {
      # support '%{getenv:HOME}' alongside '%(echo $HOME)'.
      s/$initvalue/$ENV{$arg}/g;
    } elsif ($prefix =~ /expand/) {
      # helper for parametrized macros
      s/$initvalue/expandmacros($arg)/eg;
    }
  }
  pos = 0; # reset to start of $_

  # support for **some** %if constructs: handle '%{?conditional:macros}'
  # with nested curly braces, all in one go!
CONDMAC: while (my ($qex,$macro,$value) = map { /{([?!]+)(\w+)(?::(.+))?}/ }
      bracelet() ) {
    (my $initvalue = "%{$qex$macro".(defined $value ? ":$value" : '').'}')
      =~ s/([?!{}])/\\$1/g; # neutralize quantifiers
    $value //= ($specglobals{$macro} or '');
    $value = '' unless $specglobals{$macro} xor $qex =~ /!/; # equivalence
    s/$initvalue/expandmacros($value)/eg;
  }
  pos = 0; # reset to start of $_

  simplemacros();

  # unescape multi-line macros
  s/\\"/"/g;
  s/\\\\/\\/g;
  s/(\S)\\+\n/$1\n/g;

  return $_;
} # end expandmacros()


## bracelet()
# Shortcut for extracting paired curly braces
sub bracelet {
  return extract_multiple($_, [ sub { extract_bracketed($_[0],'{}') } ]);
} # end bracelet()


## simplemacros()
# Expand 'simple' '%{macros}': %define's and %{all_the_rest}
sub simplemacros {
  return unless m/%/;
  while (/%\{([a-z_]\w+)}/ig) {
    my $key = $1;
    if (defined $specglobals{$key}) {
      s/%\{$key}/expandmacros($specglobals{$key})/eg;
    } elsif (defined $pkgdata{main}{$key}) {
      s/%\{$key}/expandmacros($pkgdata{main}{$key})/eg;
    }
  }
  pos = 0; # reset to start of $_
} # end simplemacros()

## handle_macro_options()
# Option handling for '%macro(ino:pts)'.
# Note that 'getopts()' normally works on '@ARGV', so we have to pull some
# tricks to make it work with 'string input'.
sub handle_macro_options {
  my ($inline) = @_;
  my ($macro,$argv) = $inline =~ /%(\w+)(?:\s+(.+))?/;
  local @ARGV = split ' ',$argv if $argv;
  my %options; # store result of 'getopts()'
  my @nonoptions; # store all 'other' arguments
  while (@ARGV) { # separate options and non-options
    push @nonoptions, shift @ARGV while @ARGV and $ARGV[0] !~ m/^-/;
    getopts($macroopts{$macro},\%options) if $macroopts{$macro};
  }
  local $_ = $specglobals{$macro};
  while (my ($qex,$arg,$value) = map { /{([?!]+)(\d+)(?::(.+))?}/ }
      bracelet() ) { # handle '%{?2:%{2}}' et al.
    (my $initvalue = "%{$qex$arg".(defined $value ? ":$value" : '').'}')
      =~ s/([?!{}])/\\$1/g; # neutralisation
    $value //= "%{$arg}";
    $value = '' unless defined $nonoptions[$arg-1] xor $qex =~ /!/; # equivalence
    s/$initvalue/$value/g;
  }
  pos = 0; # reset to start of $_
  while (my ($option,$repl) = map { /{-(\w):\s*(\S+)}/ } bracelet() ) {
    (my $result = $options{$option} ? $repl : '') =~ s/%\{-(\w)\*}/$options{$1}/g;
    $repl =~ s/([*{}])/\\$1/g; # mask Perlish special characters
    s/%\{-$option:\s*$repl}/$result/g;
  }
  pos = 0; # reset to start of $_
  while (my ($option) = map { /{-(\w)(?:\*)?}/ } bracelet() ) {
    my $result = $options{$option} ? "-$option" : '';
    s/%\{-$option}/$result/g;
    s/%\{-($option)\*}/$options{$1}/g;
  }
  s/%%/%/g; # reduce level of macronization
  s/\s+\\+\n/\n/g; # prepare for multi-line shipment
  s/%\{\?\*\}/join(' ',@nonoptions)/eg; # replace 'all arguments'
  for my $i (1..@nonoptions) { # fill '%{i}' parameters
    s/%\{?$i\}?/$nonoptions[$i-1]/g;
  }
  return $_;
} # end handle_macro_options()


__END__

=encoding utf8

=head1 NAME

debbuild — Build Debian-compatible .deb packages from RPM .spec files

=head1 SYNOPSIS

 debbuild {-ba|-bb|-bp|-bc|-bi|-bl|-bs} [build-options] file.spec

 debbuild {-ta|-tb|-tp|-tc|-ti|-tl|-ts} [build-options] file.{tgz|zip|tar.{gz|bz2|xz|Z}}

 debbuild {-ra|-rb|-rp|-rc|-ri|-rl|-rs} [build-options] file.{.src.rpm|sdeb}

 debbuild {--rebuild|--recompile} file.{src.rpm|sdeb}

 debbuild --showpkgs

 debbuild --nobuild file.spec

 debbuild {--install|-i} foo.sdeb

=head1 DESCRIPTION

B<debbuild> attempts to build Debian-friendly semi-native packages from RPM spec files, RPM-friendly tarballs, and RPM source packages (.src.rpm files).  It accepts I<most> of the options B<rpmbuild> does, and should be able to interpret most spec files usefully.  Perl modules should be handled via CPAN+dh-make-perl instead as it’s simpler than even tweaking a .spec template.

As far as possible, the command-line options are identical to those from B<rpmbuild>, although several B<rpmbuild> options are not supported:

 --clean
 --rmsource
 --rmspec
 --showrc
 --sign
 --target

Some of these could probably be trivially added.  Feel free to send me a patch.  ;)

Complex spec files will most likely I<not> work well, if at all.  Rewrite them from scratch – you’ll have to make heavy modifications anyway.

If you see something you don’t like, mail me.  Send a patch if you feel inspired.  I don’t promise I’ll do anything other than say “Yup, that’s broken” or “Got your message”.

The source package container I invented for B<debbuild>, the .sdeb file, can be installed with C<debbuild -i> exactly the same way as a .src.rpm can be installed with C<rpm -i>.  Both will unpack the file and place the source(s) and .spec file in the appropriate places in %_topdir/SOURCES and %_topdir/SPECS respectively.

=head1 ASSUMPTIONS

As with B<rpmbuild>, B<debbuild> makes some assumptions about your system.

=over 4

=item *

Either you have rights to do as you please under /usr/src/debian, or you have created a file F<~/.debmacros> containing a suitable “%_topdir” definition.

Both B<rpmbuild> and B<debbuild> require the directories B<%_topdir/{BUILD,SOURCES,SPECS}>.  However, where B<rpmbuild> requires the B<%_topdir/{RPMS,SRPMS}> directories, B<debbuild> requires B<%_topdir/{DEBS,SDEBS}> instead.  Create them in advance; I<some> subdirectories are created automatically as needed, but most are not.

=item *

B</var/tmp> must allow script execution – B<rpmbuild> and B<debbuild> both rely on creating and executing shell scripts for much of their functionality.  By default, B<debbuild> also creates install trees under B</var/tmp> – however this is (almost) entirely under the control of the package’s .spec file.

=item *

If you wish to B<--rebuild> a .src.rpm file, your B<%_topdir> for both B<debbuild> and B<rpmbuild> must either match, or be suitably symlinked one direction or another so that both programs are effectively working in the same tree.  (Or you could just manually wrestle files around your system.)

You could symlink F<~/.rpmmacros> to F<~/.debmacros> (or vice versa) and save yourself some hassle if you need to rebuild .src.rpm packages on a regular basis.

=back

=head1 ERRATA

B<debbuild> deliberately does a few things differently from B<rpmbuild>.

=head2 BuildArch or BuildArchitecture

B<rpmbuild> takes the last BuildArch entry it finds in the .spec file, whatever it is, and runs with that for all packages.  Debian’s repository system is fairly heavily designed around the assumption that a single source package may generate small binary (executable) packages for each arch, and large binary arch-all packages containing shared data.

B<debbuild> allows this by using the architecture specified by (in order of preference):

=over 4

=item * Host architecture

=item * BuildArch specified in .spec file preamble

=item * “Last specified” BuildArch for packages with several subpackages

=item * BuildArch specified in the %package section for that subpackage

=back

=head2 Finding out what packages should be built (--showpkgs)

B<rpmbuild> does not include any convenient method I know of to list the packages a spec file will produce.  Since I needed this ability for another tool, I added it.

It requires the .spec file for the package, and produces a list of full package filenames (without path data) that would be generated by one of B<--rebuild>, B<-ta>, B<-tb>, B<-ra>, B<-rb>, B<-ba>, or B<-bb>.  This includes the .sdeb source package.

=head1 AUTHOR

B<debbuild> was written by Kris Deugau <kdeugau@deepnet.cx>.  The present version was developed by Andreas Scherer L<http://ascherer.github.io> and is available at L<http://github.com/ascherer/debbuild>.

=head1 BUGS

Funky Things Happen if you forget a command-line option or two.  I’ve been too lazy to bother fixing this.

Many macro expansions are supported, some are incompletely supported, some not at all.

The generated scriptlets don’t quite match those from B<rpmbuild> exactly.  There are extra environment variables and preprocessing that I haven't needed (yet).

Dcumentation, such as it is, will likely remain perpetually out of date (in which way it follows in RPM’s tradition).

%_topdir and the five “working” directories under %_topdir could arguably be created by B<debbuild>.  However, B<rpmbuild> doesn't create these directories either.

=head1 SEE ALSO

rpm(8), rpmbuild(8), and pretty much any document describing how to write a .spec file.

=cut
