Below please find the `equivs-build-multi' script I use to implement
this.

It simply parses a control file into individual temporary equivs
control files, and invokes equivs-build on each in turn.

Note, however, that the license of the attached script is not GPL.
I'll be happy to discuss licensing options (dual-licensed BSD/GPL if
at all possible, or even changing the license to GPL if that will help
get the script included in the equivs package proper) -- but at this
point, I'm making the script available under the BSD license so as to
save others from reinventing the wheel.

Ideally, maybe the current equivs-build could be made into a library
and the below script refactored and adopted as the main entry point to
the current equivs-build functionality.  Having a single script to
build equivs files with one or multiple package definitions in them
would seem like the best option from a usability point of view.

#!/usr/bin/perl
#
# equivs-build-multi: build multiple packages from a single equivs control file
# era Fri Jan 26 23:07:01 2007
#
# Depends:
#  equivs
#  perl-base (perl, Getopt::Long)
#  perl-modules (File::Temp)


######## TODO: i18n


=head1 NAME

equivs-build-multi - build multiple packages from one equivs file


=head1 SYNOPSIS

B<equivs-build-multi> I<controlfile>


=head1 DESCRIPTION

B<equivs-build-multi> is a wrapper for
L<equivs-build(1)>
to build multiple packages from a single
input file of package descriptions.
The input file is similar to the basic
B<equivs> file format, with the
differences described below.

=over 2

=item *

The file may contain multiple sections.
Empty lines (one or more) are section dividers.

=item *

The first of these sections is the "global" section,
which defines defaults which will apply to all subsequent sections.
It may not contain a B<Package> field, nor B<Description>;
it must contain a B<Source> field; and it will usually define
B<Standards-Version>, B<Section>, and B<Priority> for all packages
defined in the file in the subsequent sections.

B<Maintainer>, B<Architecture>, B<Copyright> and B<Changelog>
are also usually global, if they are specified,
but B<equivs-build> will fill in the required fields
with its own defaults if they are missing
(do check, though, whether those defaults make sense for you),
and optional fields are, well, optional.

=item *

Each subsequent section is a "package" section.
It is required to have at least the
B<Package> and B<Description> fields,
and in practice, it should contain dependencies, too,
(B<Depends>, B<Suggests>, B<Recommends>,
and/or the B<Pre-> and B<Build-> variants of these)
in order to be at all meaningful.

=back

Each package section is merged
with the global section
into a separate control file for B<equivs-build>,
as described in more detail in the next paragraph,
and B<equivs-build> is invoked on that file
to actually build that package.
The temporary B<equivs-build> control file
is removed after the build,
regardless of whether the build was successful.

=for a bit later
######## TODO: make global and package dependency fields additive?

The merge algorithm for global and package-specific fields
is straightforward:
if there is a conflict, the package-specific value wins.
In other words, a package section is allowed
to override values specified in the global section.
However, a warning is generated whenever
a global field value is overridden.
>From a readability and maintainability perspective,
it is probably better to avoid overrides,
and instead specify the field's value
in each package section individually.

Each package section results in a temporary control file
which is passed to B<equivs-build> and then
removed after B<equivs-build> finishes building it.


=head1 OPTIONS

B<equivs-build-multi> offers the following options:

=over 4

=item --quiet | -q

Quiet mode.
Currently merely disables warnings whenever
package sections override global field values.


=item --keep | -k

Keep the generated B<equivs-build> input files.
Normally they are removed after they are used.

The names of the generated files are printed;
they are automatically generated
in order to be unique and never overwrite
an existing file.


=item --generate-only | -g

Only generate the B<equivs-build> file(s)
and exit.

Implies the B<--keep> option.


=item --help | -h

Print a brief help message and exit.


=item --version | -v

Print the version number of B<equivs-build-multi> and exit.


=back


=head1 EXAMPLE

The following B<equivs-build-multi> file
will result in two runs of B<equivs-build>
on two different files.
Both will contain the global values,
and then one each of the package sections.

  Source: example-local-equivs
  Section: misc
  Priority: optional
  Standards-Version: 3.5.10
  Changelog: changelog
  Version: 0.01
  Architecture: all
  Maintainer: era eriksson <[EMAIL PROTECTED]>
  Copyright: /usr/share/common-licenses/BSD

  Package: first-example
  Depends: deborphan, debconf-utils
  Description: An example which doesn't do anything very useful
   Well, it pulls in deborphan and debconf-utils,
   if you should want them on your system.

  Package: second-example
  Copyright: /usr/share/common-licenses/GPL
  Description: Another fairly useless example
   Just for demonstration purposes, it overrides the Copyright:
   from the global section (GPL vs BSD).
   .
   The description field is also a good place for comments
  Pre-Depends: first-example
  X-Random-Comment: arbitrary reasonably unique field names
   can be used to embed comments.
   .
   For visibility, however,
   putting them in the Description: field might be better,

For regular B<equivs> packages,
it is perhaps not very useful to have
detailed per-package control over fields such as
architecture (by nature, an equivs package is usually "all"),
copyright etc. but if you find you need it, the mechanism is there.


=cut


use strict;
use warnings;


package Debian::Equivs::Multi;

use Carp;

sub new
{
    my ($class, $file, $options) = @_;
    my $self = bless { file => $file, opt => $options }, $class || ref $class;
    $self->_init() || return undef;
    return $self;
}
sub _init
{
    my ($self) = @_;
    my ($file, $ctl) = $self->{file};
    unless (open ($ctl, $file))
    {
        carp "Could not open file $file: $!";
        return undef;
    }
    # else

    $self->{handle} = $ctl;
    $self->readpackages ($ctl) || return undef;
    close ($ctl);

    return $self;
}


sub readpackages
{
    my ($self, $handle) = @_;

    $self->readglobal ($handle) || return undef;

    $self->{packages} = [];
    while (! eof $handle)
    {
        $self->readpkg ($handle);
    }

    return $self;
}


sub readglobal
{
    my ($self, $handle) = @_;
    my %global = $self->readsection($handle);
    return undef unless %global;

    croak $self->{file}, ":$.: global section must specify 'Source:' field"
        unless (defined $global{'source'});
    croak $self->{file}, ":$.: global section may not contain 'Package:' field"
        if (defined $global{'package'});
    croak $self->{file},
            ":$.: global section may not contain 'Description:' field"
        if (defined $global{'description'});

    $self->{global} = \%global;
}

sub readpkg
{
    my ($self, $handle) = @_;

    my %section = $self->readsection ($handle);
    return undef unless %section;

    croak $self->{file}, ":$.: previous section did not declare Package: field"
        unless (defined $section{'package'});

    ######## TODO: maybe turn packages into subordinate objects
    my $package = $section{'package'};
    chomp $package; $package =~ s/\APackage:\s+//i;

    croak $self->{file}, ":$.: package $package already defined earlier"
        if (defined $self->{packagehash}->{$package});
    carp $self->{file}, ":$.: package $package has no description"
        unless (defined $section{'description'});

    ######## TODO: make some head keys read-only, changing them fatal?
    unless ($self->{opt}->{'quiet'})
    {
        map { carp "package $package overrides ",
            "global $_ value '$self->{global}->{$_}' with '$section{$_}'"
                if (defined $self->{global}->{$_});
        } keys %section;
    }

    # Merge globals and %section into final hash %package
    my %package = %{$self->{global}};
    map { $package{$_} = $section{$_} } keys %section;

    $package{"_packagename"} = $package;
    push @{$self->{packages}}, \%package;
    $self->{packagehash}->{$package} = \%package;

    return %package;
}

sub readsection
{
    my ($self, $handle) = @_;

    my (%field, $field);

    while ($_ = <$handle>)
    {
        last if ($_ eq "\n");

        if (m/^\s/)
        {
            chomp, croak $self->{file}, ":$.: Malformed input line '$_'"
                unless $field;
            $field{$field} .= $_;
        }
        elsif (m/^(([A-Za-z][-0-9A-Za-z]*):.*\n?)$/)
        {
            $field = lc($2);
            croak $self->{file}, ":$.: section already defined $field earlier"
                if (defined $field{$field});
            $field{$field} = $1;
        }
        else
        {
            croak $self->{file}, ":$.: Malformed input file";
        }
    }

    return %field;
}


sub _equivs_build
{
    my ($self, $file) = @_;
    my $exitcode = system ("equivs-build", $file);
    unlink $file unless ($self->{opt}->{'keep'});
    croak "equivs-build failed (exit code ", $exitcode >> 8, ")"
        unless ($exitcode == 0);
}

sub build
{
    my ($self, @packages) = @_;

    if (@packages)
    {
        # Check that all requested packages are also available
        my %requested = map { $_ => 1 } @packages;
        map { delete $requested{$_} } keys %{$self->{packagehash}};
        croak "Cannot build package", (keys %requested > 1 ? "s" : ""), " ",
            join (", ", keys %requested) if %requested;
    }
    else
    {
        @packages = keys %{$self->{packagehash}};
    }

    my %build = map { $_ => 1 } @packages;

    for my $pkg (@{$self->{packages}})
    {
        next unless $build{$pkg->{'_packagename'}};
        my $file = $self->writepackage ($pkg);
        print "$file\n"
            if ($self->{opt}->{'generate-only'} || $self->{opt}->{'keep'});
        $self->_equivs_build($file) unless ($self->{opt}->{'generate-only'});
    }
}

use File::Temp qw(tempfile);

sub writepackage
{
    my ($self, $pkgref) = @_;

    my ($package) = $pkgref->{"_packagename"};
    my ($template) = join (".", "equivs", $package, "tmpXXXXX");

    ######## TODO: option to use a different directory for temporary files
    my ($handle, $filename) = tempfile($template, DIR => ".");

    $self->writesection ($handle, $pkgref,
        [qw(Source Section Priority Standards-Version)],
        [qw(Changelog Copyright)]);
    print $handle "\n";
    $self->writesection ($handle, $pkgref, [qw(Package Version)],
        [qw(Architecture Maintainer Depends Description)]);

    map { print $handle $pkgref->{$_}
        unless $_ eq '_packagename' } keys %{$pkgref};

    close ($handle);
    return $filename;
}

sub writesection
{
    my ($self, $handle, $keyhash, $reqarray, $optarray) = @_;

    for my $field (map { lc } @{$reqarray})
    {
        if (defined $keyhash->{$field})
        {
            print $handle $keyhash->{$field};
            delete $keyhash->{$field};
        }
        else
        {
            carp "package $keyhash->{_packagename}: ",
                "required field $field missing";
        }
    }
    for my $field (map { lc } @{$optarray})
    {
        if (defined $keyhash->{$field})
        {
            print $handle $keyhash->{$field};
            delete $keyhash->{$field};
        }
    }
}


our $VERSION = 0.01;


package main;

unless (caller)
{
    our $me = $0;
    $me =~ s,.*/,,;

    my $syntax = "Syntax: $me [-gkq] controlfile [ packages ] | -[vh]\n";

    use Getopt::Long;
    Getopt::Long::Configure ("bundling", "no_ignore_case");
    my %opt = ();
    GetOptions (\%opt,
        "keep|k", "quiet|q", "generate-only|g", "version|v", "help|h")
            || die $syntax;

    if ($opt{version})
    {
        print "$me version $Debian::Equivs::Multi::VERSION\n";
        exit 1;
    }
    # else
    if ($opt{help})
    {
        print $syntax;
        exit 1;
    }
    # else

    my $file = shift || die $syntax;
    my $packages = Debian::Equivs::Multi->new ($file, \%opt);
    die "$me: Invalid input file $file, no packages defined\n"
        unless (defined $packages);
    $packages->build (@ARGV);
}
else
{
    1;
}

######## TODO: FILES?, ENVIRONMENT?

=head1 BUGS

Proper changelog generation is currently blocked by equivs bug #409557
L<http://bugs.debian.org/409557>

=head1 AUTHOR

era eriksson
L<http://www.iki.fi/era/>


=head1 LICENSE

"New-style" BSD (no advertising clause).


=cut
######## FIXME: include a license in the source
#See the source for a copy of the full license text.

Reply via email to