1#!/usr/bin/perl
2
3# mk-build-deps: make a dummy package to satisfy build-deps of a package
4# Copyright 2008 by Vincent Fourmond
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19
20# Changes:
21# * (Vincent Fourmond 4/4/2008): now take Build-Depends-Indep
22#   into consideration
23
24=head1 NAME
25
26mk-build-deps - build a package satisfying a package's build-dependencies
27
28=head1 SYNOPSIS
29
30B<mk-build-deps> B<--help>|B<--version>
31
32B<mk-build-deps> [I<options>] I<control file> | I<package name> ...
33
34=head1 DESCRIPTION
35
36Given a I<package name> and/or I<control file>, B<mk-build-deps>
37will use B<equivs> to generate a binary package which may be installed to
38satisfy all the build dependencies of the given package.
39
40If B<--build-dep> and/or B<--build-indep> are given, then the resulting binary
41package(s) will depend solely on the Build-Depends/Build-Depends-Indep
42dependencies, respectively.
43
44=head1 OPTIONS
45
46=over 4
47
48=item B<-i>, B<--install>
49
50Install the generated packages and its build-dependencies.
51
52=item B<-t>, B<--tool>
53
54When installing the generated package use the specified tool.
55(default: B<apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends>)
56
57=item B<-r>, B<--remove>
58
59Remove the package file after installing it. Ignored if used without
60the B<--install> switch.
61
62=item B<-a> I<foo>, B<--arch> I<foo>
63
64Set the architecture of the produced binary package to I<foo>. If this option
65is not given, fall back to the value given by B<--host-arch>. If neither this
66option nor B<--host-arch> are given but the Build-Depends contain architecture
67restrictions, use the value printed by `dpkg-architecture -qDEB_HOST_ARCH`.
68Otherwise, use I<all>.
69
70The package architecture must be equal to the host architecture except if the
71package architecture is I<all>.
72
73The package architecture cannot be I<all> if the build and host architecture
74differ.
75
76=item B<--host-arch> I<foo>
77
78Set the host architecture the binary package is built for. This defaults to
79the value printed by `dpkg-architecture -qDEB_HOST_ARCH`. Use this option to
80create a binary package that is able to satisfy crossbuild dependencies.
81
82If this option is used together with B<--arch>, then they must be equal except
83if the value of B<--arch> is I<all>.
84
85If B<--arch> is not given, then this option also sets the package architecture.
86
87=item B<--build-arch> I<foo>
88
89Set the build architecture the binary package is built for. This defaults to
90the value printed by `dpkg-architecture -qDEB_BUILD_ARCH`. Use this option to
91create a binary package that is able to satisfy crossbuild dependencies.
92
93=item B<-B>, B<--build-dep>
94
95Generate a package which only depends on the source package's Build-Depends
96dependencies.
97
98=item B<-A>, B<--build-indep>
99
100Generate a package which only depends on the source package's
101Build-Depends-Indep dependencies.
102
103=item B<-P>, B<--build-profiles> I<profile[,...]>
104
105Generate a package which only depends on build dependencies
106with the build profile(s), given as a comma-separated list.
107The default behavior is to use no specific profile.
108Setting this option will override the B<DEB_BUILD_PROFILES>
109environment variable.
110
111=item B<-h>, B<--help>
112
113Show a summary of options.
114
115=item B<-v>, B<--version>
116
117Show version and copyright information.
118
119=item B<-s>, B<--root-cmd>
120
121Use the specified tool to gain root privileges before installing.
122Ignored if used without the B<--install> switch.
123
124=back
125
126=head1 ENVIRONMENT
127
128=head2 External environment
129
130=over 4
131
132=item B<DEB_BUILD_PROFILES>
133
134If set, it will be used as the active build profile(s) for the
135build dependencies to be installed.
136It is a space separated list of profile names.
137Overridden by the B<-P> option.
138
139=back
140
141=head1 AUTHOR
142
143B<mk-build-deps> is copyright by Vincent Fourmond and was modified for the
144devscripts package by Adam D. Barratt <adam@adam-barratt.org.uk>.
145
146This program comes with ABSOLUTELY NO WARRANTY.
147You are free to redistribute this code under the terms of the GNU
148General Public License, version 2 or later.
149
150=cut
151
152use 5.01;
153use strict;
154use warnings;
155use Getopt::Long qw(:config bundling permute no_getopt_compat);
156use File::Basename;
157use Pod::Usage;
158use Dpkg::Control;
159use Dpkg::Version;
160use Dpkg::IPC;
161use Dpkg::Deps;
162use FileHandle;
163use Text::ParseWords;
164
165my $progname = basename($0);
166my $opt_install;
167my $opt_remove = 0;
168my ($opt_help, $opt_version, $opt_arch, $opt_dep, $opt_indep, $opt_hostarch,
169    $opt_buildarch, $opt_buildprofiles);
170my $control;
171my $install_tool;
172my $root_cmd;
173my @packages;
174
175my @config_files = ('/etc/devscripts.conf', '~/.devscripts');
176my %config_vars  = (
177    'MKBUILDDEPS_TOOL' =>
178'/usr/bin/apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends',
179    'MKBUILDDEPS_REMOVE_AFTER_INSTALL' => 'no',
180    'MKBUILDDEPS_ROOTCMD'              => '',
181);
182my %config_default = %config_vars;
183
184my $shell_cmd;
185# Set defaults
186foreach my $var (keys %config_vars) {
187    $shell_cmd .= qq[$var="$config_vars{$var}";\n];
188}
189$shell_cmd .= 'for file in ' . join(" ", @config_files) . "; do\n";
190$shell_cmd .= '[ -f $file ] && . $file; done;' . "\n";
191# Read back values
192foreach my $var (keys %config_vars) { $shell_cmd .= "echo \$$var;\n" }
193my $shell_out = `/bin/bash -c '$shell_cmd'`;
194@config_vars{ keys %config_vars } = split /\n/, $shell_out, -1;
195
196# Check validity
197$config_vars{'MKBUILDDEPS_TOOL'} =~ /./
198  or $config_vars{'MKBUILDDEPS_TOOL'} = $config_default{'MKBUILDDEPS_TOOL'};
199$config_vars{'MKBUILDDEPS_REMOVE_AFTER_INSTALL'} =~ /^(yes|no)$/
200  or $config_vars{'MKBUILDDEPS_REMOVE_AFTER_INSTALL'}
201  = $config_default{'MKBUILDDEPS_REMOVE_AFTER_INSTALL'};
202$config_vars{'MKBUILDDEPS_ROOTCMD'} =~ /./
203  or $config_vars{'MKBUILDDEPS_ROOTCMD'}
204  = $config_default{'MKBUILDDEPS_ROOTCMD'};
205
206$install_tool = $config_vars{'MKBUILDDEPS_TOOL'};
207
208if ($config_vars{'MKBUILDDEPS_REMOVE_AFTER_INSTALL'} =~ /yes/) {
209    $opt_remove = 1;
210}
211
212sub usage {
213    my ($exitval) = @_;
214
215    my $verbose = $exitval ? 0 : 1;
216    pod2usage({ -exitval => 'NOEXIT', -verbose => $verbose });
217
218    if ($verbose) {
219        my $modified_conf_msg;
220        foreach my $var (sort keys %config_vars) {
221            if ($config_vars{$var} ne $config_default{$var}) {
222                $modified_conf_msg .= "  $var=$config_vars{$var}\n";
223            }
224        }
225        $modified_conf_msg ||= "  (none)\n";
226        chomp $modified_conf_msg;
227        print <<EOF;
228Default settings modified by devscripts configuration files:
229$modified_conf_msg
230EOF
231    }
232
233    exit $exitval;
234}
235
236GetOptions(
237    "help|h"             => \$opt_help,
238    "version|v"          => \$opt_version,
239    "install|i"          => \$opt_install,
240    "remove|r"           => \$opt_remove,
241    "tool|t=s"           => \$install_tool,
242    "arch|a=s"           => \$opt_arch,
243    "host-arch=s"        => \$opt_hostarch,
244    "build-arch=s"       => \$opt_buildarch,
245    "build-dep|B"        => \$opt_dep,
246    "build-indep|A"      => \$opt_indep,
247    "build-profiles|P=s" => \$opt_buildprofiles,
248    "root-cmd|s=s"       => \$root_cmd,
249) or usage(1);
250
251usage(0) if ($opt_help);
252
253if ($opt_version) { version(); exit 0; }
254
255if (!@ARGV) {
256    if (-r 'debian/control') {
257        push(@ARGV, 'debian/control');
258    }
259}
260
261usage(1) unless @ARGV;
262
263system("command -v equivs-build >/dev/null 2>&1");
264if ($?) {
265    die "$progname: You must have equivs installed to use this program.\n";
266}
267
268while ($control = shift) {
269    my ($name, $fh, $descr, $pid);
270    if (-r $control and -f $control) {
271        open $fh, $control;
272        unless (defined $fh) {
273            warn "Unable to open $control: $!\n";
274            next;
275        }
276        $name  = 'Source';
277        $descr = "control file `$control'";
278    } else {
279        $fh  = FileHandle->new();
280        $pid = spawn(
281            exec      => ['apt-cache', 'showsrc', $control],
282            from_file => '/dev/null',
283            to_pipe   => $fh
284        );
285        unless (defined $pid) {
286            warn "Unable to run apt-cache: $!\n";
287            next;
288        }
289        $name  = 'Package';
290        $descr = "`apt-cache showsrc $control'";
291    }
292
293    my (@pkgInfo, @versions);
294    until (eof $fh) {
295        my $ctrl = Dpkg::Control->new(allow_pgp => 1, type => CTRL_UNKNOWN);
296        # parse() dies if the file isn't syntactically valid and returns undef
297        # if there simply weren't any fields parsed
298        unless ($ctrl->parse($fh, $descr)) {
299            warn "$progname: Unable to find package name in $descr\n";
300            next;
301        }
302        unless (exists $ctrl->{$name}) {
303            next;
304        }
305        my $args = '';
306        my $arch = 'all';
307        my ($build_deps, $build_dep, $build_dep_arch, $build_indep);
308        my ($build_conflicts, $build_conflict, $conflict_arch,
309            $conflict_indep);
310
311        if (exists $ctrl->{'Build-Depends'}) {
312            $build_dep = $ctrl->{'Build-Depends'};
313            $build_dep =~ s/\n/ /g;
314            $build_deps = $build_dep;
315        }
316        if (exists $ctrl->{'Build-Depends-Arch'}) {
317            $build_dep_arch = $ctrl->{'Build-Depends-Arch'};
318            $build_dep_arch =~ s/\n/ /g;
319            $build_dep  .= ', ' if $build_dep;
320            $build_dep  .= $build_dep_arch;
321            $build_deps .= ', ' if $build_deps;
322            $build_deps .= $build_dep_arch;
323        }
324        if (exists $ctrl->{'Build-Depends-Indep'}) {
325            $build_indep = $ctrl->{'Build-Depends-Indep'};
326            $build_indep =~ s/\n/ /g;
327            $build_deps .= ', ' if $build_deps;
328            $build_deps .= $build_indep;
329        }
330        if (exists $ctrl->{'Build-Conflicts'}) {
331            $build_conflict = $ctrl->{'Build-Conflicts'};
332            $build_conflict =~ s/\n/ /g;
333            $build_conflicts = $build_conflict;
334        }
335        if (exists $ctrl->{'Build-Conflicts-Arch'}) {
336            $conflict_arch = $ctrl->{'Build-Conflicts-Arch'};
337            $conflict_arch =~ s/\n/ /g;
338            $build_conflict  .= ', ' if $build_conflict;
339            $build_conflict  .= $conflict_arch;
340            $build_conflicts .= ', ' if $build_conflicts;
341            $build_conflicts .= $conflict_arch;
342        }
343        if (exists $ctrl->{'Build-Conflicts-Indep'}) {
344            $conflict_indep = $ctrl->{'Build-Conflicts-Indep'};
345            $conflict_indep =~ s/\n/ /g;
346            $build_conflicts .= ', ' if $build_conflicts;
347            $build_conflicts .= $conflict_indep;
348        }
349
350        warn "$progname: Unable to find build-deps for $ctrl->{$name}\n"
351          unless $build_deps;
352
353        if (exists $ctrl->{Version}) {
354            push(@versions, $ctrl->{Version});
355        } elsif ($name eq 'Source') {
356            (my $changelog = $control) =~ s@control$@changelog@;
357            if (-f $changelog) {
358                require Dpkg::Changelog::Parse;
359                my $log = Dpkg::Changelog::Parse::changelog_parse(
360                    file => $changelog);
361                if ($ctrl->{$name} eq $log->{$name}) {
362                    $ctrl->{Version} = $log->{Version};
363                    push(@versions, $log->{Version});
364                }
365            }
366        }
367
368        # Only build a package with both B-D and B-D-I in Depends if the
369        # B-D/B-D-I specific packages weren't requested
370        if (!($opt_dep || $opt_indep)) {
371            push(
372                @pkgInfo,
373                {
374                    depends   => $build_deps,
375                    conflicts => $build_conflicts,
376                    name      => $ctrl->{$name},
377                    type      => 'build-deps',
378                    version   => $ctrl->{Version} });
379            next;
380        }
381        if ($opt_dep) {
382            push(
383                @pkgInfo,
384                {
385                    depends   => $build_dep,
386                    conflicts => $build_conflict,
387                    name      => $ctrl->{$name},
388                    type      => 'build-deps-depends',
389                    version   => $ctrl->{Version} });
390        }
391        if ($opt_indep) {
392            push(
393                @pkgInfo,
394                {
395                    depends   => $build_indep,
396                    conflicts => $conflict_indep,
397                    name      => $ctrl->{$name},
398                    type      => 'build-deps-indep',
399                    version   => $ctrl->{Version} });
400        }
401    }
402    wait_child($pid, nocheck => 1) if defined $pid;
403    # Only use the newest version.  We'll only have this if processing showsrc
404    # output or a dsc file.
405    if (@versions) {
406        @versions = map { $_->[0] }
407          sort { $b->[1] <=> $a->[1] }
408          map { [$_, Dpkg::Version->new($_)] } @versions;
409        push(@packages,
410            map { build_equiv($_) }
411            grep { $versions[0] eq $_->{version} } @pkgInfo);
412    } elsif (@pkgInfo) {
413        push(@packages, build_equiv($pkgInfo[0]));
414    } else {
415        die "$progname: Unable to find package name in $descr\n";
416    }
417}
418
419if ($opt_install) {
420    my @root;
421    if ($root_cmd) {
422        push(@root, shellwords($root_cmd));
423    }
424
425    my (@pkg_names, @deb_files, %uniq);
426    for my $package (@packages) {
427        if ($uniq{ $package->{deb_file} }++ == 0) {
428            push @pkg_names, $package->{package};
429            push @deb_files, $package->{deb_file};
430        }
431    }
432
433    system @root, 'dpkg', '--unpack', @deb_files;
434    die("$progname: dpkg --unpack failed\n") if (($? >> 8) != 0);
435    system @root, shellwords($install_tool), '-f', 'install';
436    my $err = $? >> 8;
437    if (!$err) {
438        # $install_tool succeeded. Did the packages get installed? It's
439        # possible that they didn't because $install_tool may have realized
440        # that installation was impossible, and it could have given up,
441        # successfully.
442        for (my $i = 0 ; $i < @pkg_names ; $i++) {
443            my $pkg = $pkg_names[$i];
444            my $status;
445            spawn(
446                exec =>
447                  ['dpkg-query', '-W', '-f', '${db:Status-Status}', $pkg],
448                to_string     => \$status,
449                error_to_file => '/dev/null',
450                nocheck       => 1,
451                wait_child    => 1
452            );
453            if ($status ne 'installed' || ($? >> 8)) {
454                # Restore system to previous state, since $install_tool wasn't
455                # able to resolve a proper way to get the build-dep packages
456                # installed
457                warn "$progname: Unable to install $pkg";
458                $err = 1;
459            } elsif ($opt_remove) {
460                unlink $deb_files[$i];
461            }
462        }
463        if ($err) {
464            die "$progname: Unable to install all build-dep packages\n";
465        }
466    } else {
467        # Restore system to previous state, since $install_tool wasn't able to
468        # resolve a proper way to get the build-dep packages installed
469        system @root, 'dpkg', '--remove', @pkg_names;
470        die("$progname: Unable to install all build-dep packages\n");
471    }
472}
473
474sub version {
475    print <<"EOF";
476This is $progname, from the Debian devscripts package, version ###VERSION###
477Copyright (C) 2008 Vincent Fourmond
478
479This program comes with ABSOLUTELY NO WARRANTY.
480You are free to redistribute this code under the terms of the
481GNU General Public License, version 2, or (at your option) any
482later version.
483EOF
484}
485
486sub build_equiv {
487    my ($opts) = @_;
488    my $args = '';
489
490    my $packagearch = 'all';
491
492    if (defined $opt_arch) {
493        $packagearch = $opt_arch;
494    } elsif (defined $opt_hostarch) {
495        $packagearch = $opt_hostarch;
496    } elsif ($opts->{depends} =~ m/\[|\]/) {
497        chomp($packagearch = `dpkg-architecture -qDEB_HOST_ARCH`);
498    }
499    if ($packagearch ne "all") {
500        $args .= "--arch=$packagearch ";
501    }
502
503    chomp(my $buildarch = `dpkg-architecture -qDEB_BUILD_ARCH`);
504    if (defined $opt_buildarch) {
505        $buildarch = $opt_buildarch;
506    }
507
508    chomp(my $hostarch = `dpkg-architecture -qDEB_HOST_ARCH`);
509    if (defined $opt_hostarch) {
510        $hostarch = $opt_hostarch;
511    }
512
513    if ($packagearch eq "all") {
514        if ($buildarch ne $hostarch) {
515            die
516"build architecture \"$buildarch\" is unequal host architecture \"$hostarch\" in which case the package architecture must not be \"all\" (but \"$hostarch\" instead)\n";
517        }
518    } elsif ($packagearch ne $hostarch) {
519        die
520"The package architecture must be equal to the host architecture except if the package architecture is \"all\"\n";
521    }
522
523    my $build_profiles = [split /\s+/, ($ENV{'DEB_BUILD_PROFILES'} // "")];
524    if (defined $opt_buildprofiles) {
525        $build_profiles = [split /,/, $opt_buildprofiles];
526    }
527
528    my $positive = deps_parse(
529        $opts->{depends} // "",
530        reduce_arch     => 1,
531        host_arch       => $hostarch,
532        build_arch      => $buildarch,
533        build_dep       => 1,
534        reduce_profiles => 1,
535        build_profiles  => $build_profiles
536    );
537    my $negative = deps_parse(
538        $opts->{conflicts} // "",
539        reduce_arch     => 1,
540        host_arch       => $hostarch,
541        build_arch      => $buildarch,
542        build_dep       => 1,
543        union           => 1,
544        reduce_profiles => 1,
545        build_profiles  => $build_profiles
546    );
547
548    # either remove :native for native builds or replace it by the build
549    # architecture
550    my $handle_native_archqual = sub {
551        my ($dep) = @_;
552        if ($dep->{archqual} && $dep->{archqual} eq "native") {
553            if ($hostarch eq $buildarch) {
554                $dep->{archqual} = undef;
555            } else {
556                $dep->{archqual} = $buildarch;
557            }
558        }
559        return 1;
560    };
561    deps_iterate($positive, $handle_native_archqual);
562    deps_iterate($negative, $handle_native_archqual);
563
564    my $pkgname;
565    my $buildess = "build-essential:$buildarch";
566    if ($buildarch eq $hostarch) {
567        $pkgname = "$opts->{name}-$opts->{type}";
568    } else {
569        $pkgname = "$opts->{name}-cross-$opts->{type}";
570        $buildess .= ", crossbuild-essential-$hostarch:$buildarch";
571    }
572
573    my $readme = '/usr/share/devscripts/templates/README.mk-build-deps';
574    open EQUIVS, "| equivs-build $args-"
575      or die "$progname: Failed to execute equivs-build: $!\n";
576    print EQUIVS "Section: devel\n"
577      . "Priority: optional\n"
578      . "Standards-Version: 3.7.3\n\n"
579      . "Package: $pkgname\n"
580      . "Architecture: $packagearch\n"
581      . "Depends: $buildess, $positive\n";
582
583    print EQUIVS "Conflicts: $negative\n" if $negative;
584
585    # Allow the file not to exist to ease testing
586    print EQUIVS "Readme: $readme\n" if -r $readme;
587
588    my $version = '1.0';
589    if (defined $opts->{version}) {
590        $version = $opts->{version};
591    }
592    print EQUIVS "Version: $version\n";
593
594    print EQUIVS "Description: build-dependencies for $opts->{name}\n"
595      . " Dependency package to build the '$opts->{name}' package\n";
596
597    close EQUIVS;
598
599    my $v = Dpkg::Version->new($version);
600    # The version in the .deb filename will not contain the epoch
601    $version = $v->as_string(omit_epoch => 1);
602    my $deb_file = "${pkgname}_${version}_${packagearch}.deb";
603    return {
604        package  => $pkgname,
605        deb_file => $deb_file
606    };
607}
608