#!/usr/bin/perl # emdebuild -- Emdebian version of debuild # # Checks if a package is emdebianised and runs em_make if not. # Otherwise, updates emdebian-rules.patch, # rebuilds the package using dpkg-buildpackage -sa -rfakeroot -a$arch # and generates a debuild-style build log. # # Copyright 2006-2008 Neil Williams # Copyright 1998-2006 Craig Small # Copyright 1999-2003 Julian Gilbey # # This package 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 3 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, see . # # note: running debian/rules alone is unsupported due to the cross-build. # Most debuild options are not supported - in particular, lintian and linda are not # supported. use Carp; use Cwd; use Debian::Debhelper::Dh_Lib; use Debian::DpkgCross; use Emdebian::Tools; use Dpkg::Deps; use File::HomeDir; use Text::Diff; use Text::Wrap; use Term::ANSIColor qw(:constants); use strict; use warnings; use vars qw/@packages $username $email $date $native $emdebvers $emN $addsource $progname $arch $verbose @options $dpkg_extra $conf $home $changes $ourversion $emvers $increment $source $vers $svnci $dosign $onlysvn *BUILD *OLDOUT *OLDERR %archtable $numchecks $host $wraplibtool $checkdeps $suite $vendor $vopts /; $ourversion = &tools_version(); $progname = basename($0); # workaround a bug in Debhelper::Dh_Lib #478719 if (defined &inhibit_log) { &inhibit_log; } sub usageversion { print(STDERR <my_home; $home = cwd if (!$home); &read_config(); $arch = &get_architecture(); $addsource = ""; $dosign = "false"; $onlysvn = "false"; while( @ARGV ) { $_= shift( @ARGV ); last if m/^--$/; if (!/^-/) { unshift(@ARGV,$_); last; } elsif (/^(-\?|-h|--help|--version)$/) { &usageversion; exit( 0 ); } elsif (/^(--build-dep|--build-deps)$/) { $checkdeps = 1; } elsif (/^(-v|--verbose)$/) { $verbose++; } elsif (/^(-V|--vendor)$/) { $vendor = shift; } elsif (/^(-q|--quiet)$/) { $verbose--; } elsif (/^(-a|--arch)$/) { $arch = shift(@ARGV); } elsif (/^(-n|--next)$/) { $increment = 1; } elsif (/^(--svn)$/) { $svnci = "true"; } elsif (/^(--sign)$/) { $dosign = "true"; } elsif (/^(-S|--suite)$/) { $suite = shift(@ARGV); } elsif (/^(--svn-only)$/) { $onlysvn = "true"; } else { &usageversion(); die RED, "unrecognised option: $_.", RESET . "\n"; } } $dpkg_extra = "-us -uc "; foreach my $dopt (@ARGV) { if ($dopt !~ /^\-/) { warn (RED, "invalid option for dpkg-buildpackage: $dopt", RESET, "\n"); &usageversion; exit (2); } next if ($dopt =~ /-a(.*)/); $dpkg_extra .= $dopt . " "; } undef @ARGV; $suite = (defined $suite) ? " -S $suite " : ""; my $target_gnu_type = &check_arch($arch) if (defined $arch); if ((not defined $arch)||($arch eq "")||(not defined $target_gnu_type)) { warn (RED, "\n$progname: Cannot determine the architecture to build.", RESET, "\n\n"); &usageversion; exit (1); } $host = `dpkg-architecture -qDEB_HOST_ARCH_CPU`; $host =~ s/_/-/; chomp($host); if ($checkdeps > 0) { # leave build-deps to apt for native builds. &crossbuilddeps; exit (0); } my @config_files = ('/etc/devscripts.conf', "$home/.devscripts"); foreach $conf (@config_files) { my $val; next if (! -f $conf); print CYAN, "Reading devscripts configuration in $conf.\n", RESET if ($verbose >= 3); open (CONF, $conf) or warn "Cannot read $conf: $!\n"; @options=; close CONF; foreach $val (@options) { if ($val =~ /^$dpkg_opts_var="(.*)"\n$/) { $dpkg_extra .= " " . $1; } } } print CYAN, "Checking for debian/control\n", RESET if ($verbose >= 2); &check_emdebian_control; &init; @packages = getpackages(); die (RED, "Unable to identify main package using debhelper!", RESET, "\n") unless $dh{MAINPACKAGE}; $native = isnative($dh{MAINPACKAGE}); my $package = $dh{MAINPACKAGE}; my %archchk=(); open (CONTROL, 'debian/control') or die ("Cannot read debian/control: $!\n"); while () { chomp; s/\s+$//; $archchk{$arch}++ if (/^Architecture:.*$arch.*$/); $archchk{$arch}++ if (/^Architecture:\sany$/); } close (CONTROL); warn (RED, "$progname: Warning. '$package' provides no '$arch' packages.",RESET,"\n") if (not defined $archchk{$arch}); # cache files are deprecated - only use when testing new packages my $cachefile = "emdebian-${target_gnu_type}.cache.patch"; if ($onlysvn eq "true") { # exit the working directory first. chdir ("../") if ( -f "debian/control"); # cannot pass $build here - determining the build log filename # may involve changing the patch files or emdebian version. # globbing may be unreliable. &handle_svn('',$cachefile); exit; } my $parse = `parsechangelog`; $parse =~ /Source: (.*)\n/; $source = $1; $source = $dh{MAINPACKAGE} if (!$source); $parse =~ /(Version: .*)\n/; my $deb_version = $1; print GREEN, "Building '$source' for $arch on $host.\n", RESET if ($verbose >= 1); $emvers = &extract_emdebversion($deb_version); if ($increment) { # when incrementing, any xcontrol messages remain in em1 print GREEN, "Incrementing emdebian version.\n", RESET if ($verbose >= 2); $vers = emdeb_versionstring("next"); system "debchange -p -v $vers \"New emdebian release.\""; $parse = `parsechangelog`; $parse =~ /(Version: .*)\n/; $deb_version = $1; $emvers = &extract_emdebversion($deb_version); } if ($emvers eq "") { # run em_make via the emdebian-tools package. my $maker = ($verbose >= 2) ? "em_make -v -a $arch" : "em_make -a $arch"; ( -f "/usr/bin/em_make") ? system "$maker" : die (RED, "Please run em_make before emdebuild.", RESET, "\n"); $parse = `parsechangelog`; $parse =~ /(Version: .*)\n/; $deb_version = $1; $emvers = &extract_emdebversion($deb_version); } if ($emvers eq "em1") { my $orig_msg = "New emdebian release detected, using '-sa' to include .orig.tar.gz\n"; print GREEN, wrap('','',$orig_msg), RESET if ($verbose >=2); $addsource = '-sa'; } # update the patches to reflect any manual changes. # this includes copying patches added to debian/patches &create_patches($source); # ensure any hand-edited cache file is preserved - debian/rules will need it. if (-f "${target_gnu_type}.cache") { print CYAN, "Protecting cache file ${target_gnu_type}.cache . . \n", RESET if ($verbose >=2); my $file = "${target_gnu_type}.cache"; # protect the cache file chmod 0444, $file; my $cwd = cwd; my $working = basename($cwd); chdir ("../$source.old/"); # ensure the patch works from an empty cache file in .old open (CPY, ">$file") or die "Cannot write ../$source.old/$file: $!"; close CPY; chdir ("../"); my $diff = diff "$source.old/$file", "$working/$file", { STYLE => "Unified" }; open (PATCHFILE, ">emdebian-$file.patch") or warn ("Cannot open emdebian-$file.patch : $!\n"); print PATCHFILE $diff; close (PATCHFILE); chdir ("$cwd"); print GREEN, "$file backed up to ../$source.old/\n", RESET if ($verbose >=2); } $emvers = &emdeb_versionstring("next") if ($increment); $emvers = &emdeb_versionstring("") if (!$increment); $changes = "${source}_${emvers}_${arch}.changes"; # noepoch = version without epoch and colon my $noepoch = ""; if ($emvers =~ /^[0-9]?:.*/) { $emvers =~ /^[0-9]?:(.*)/; $noepoch = $1; $changes = "${source}_${noepoch}_${arch}.changes"; } my $build="${source}_${emvers}_${arch}.build"; print CYAN, "Logging build messages to ../$build\n", RESET if ($verbose >= 2); print GREEN, "Workarounds in use are:\n", RESET if ($verbose >= 2); my $crossprefix = &check_arch($arch); my $crossbin; # setup gccross here. # A little explanation. This is needed only for linking internal # libraries within the package. PKG_CONFIG_LIBDIR deals with the rest. # This is now an opt-in with [X-]Build-Cross-Libtool: yes $wraplibtool = 0; if (-f "debian/xcontrol") { open (CONTROL, 'debian/xcontrol') or die ("Cannot read debian/xcontrol: $!\n"); while () { chomp; s/\s+$//; $wraplibtool = 1 if (/^[X-]+Build-Cross-Libtool:\s*yes/); } close (CONTROL); } if ($wraplibtool == 1) { my $dpkgcrosspath = "/usr/share/emdebian-tools/bin"; $crossbin = `mktemp -d -t gccross.XXXXXXXXXX`; chomp($crossbin); print RED, wrap ('','',"Warning! LIB WORKAROUND in use. Using gccross symlinks ". "in $crossbin. See emdebuild (1).\n"), RESET if ($verbose >= 1); print CYAN, wrap ('','', "Remove 'X-Build-Cross-Libtool: yes' from 'debian/xcontrol'." . "when '$source' is fixed. See emdebuild (1)."), RESET . "\n" if ($verbose >= 2); my $path = $ENV{PATH}; $ENV{ARCH} = $arch; $ENV{PATH} = "$dpkgcrosspath:$path"; my @pdirs = split (/:/, $ENV{PATH}); foreach my $d (@pdirs) { next if (-l "$d"); my @pathfiles = (); next unless ( -d $d); opendir (PDIR, $d) or warn ("unable to read $d"); @pathfiles=grep(!/^\.\.?$/, readdir PDIR); closedir (PDIR); foreach my $file (@pathfiles) { next if ("$file" eq "gccross"); next if ( not -x "$d/$file"); next if ( -z "$d/$file"); next if ( -d "$d/$file"); if ($file =~ /^$crossprefix\-(gcc|cpp|c\+\+|g\+\+|cc|CC)(\-[.0-9]+)*/) { print GREEN, "symlinking $file to gccross\n", RESET if ($verbose >= 3); system ("ln -s $dpkgcrosspath/gccross $crossbin/$file\n") } } } $path = $ENV{PATH}; $ENV{PATH} = "$crossbin:$path:/usr/$crossprefix/bin/"; } print GREEN, "PATH reset to $ENV{PATH}\n", RESET if (($verbose >= 2) and ($wraplibtool == 1)); # get the dpkg-vendor value and get the options for emgrip. $vendor = $ENV{"DEB_VENDOR"} if (defined $ENV{"DEB_VENDOR"} and (not defined $vendor)); $vendor = "emdebian-crush" if (not defined $vendor); chomp ($vendor); $vopts = `dpkg-vendor --vendor $vendor --query grip-build-option`; chomp ($vopts); # change only once dpkg-cross config files migrate into dpkg-dev. my $arch_opt = ""; if ($host ne $arch) { # $ENV{CONFIG_SITE}="/etc/dpkg/cross-config.$arch"; $ENV{CONFIG_SITE}="/etc/dpkg-cross/cross-config.$arch"; $arch_opt = "-a$arch"; } system ("dpkg-architecture $arch_opt"); unlink ("debian/files") if (-f "debian/files"); # Always create .build logs, emulate how debuild works # dpkg-buildpackage doesn't log by default! # from debuild: # Start by duping STDOUT and STDERR open OLDOUT, ">&STDOUT" or croak "cannot duplicate stdout: $!\n"; open OLDERR, ">&STDERR" or croak "cannot duplicate stderr: $!\n"; open BUILD, "| tee ../$build" or croak "could not open pipe to tee $build: $!"; close STDOUT; close STDERR; open STDOUT, ">&BUILD" or croak "cannot reopen stdout: $!"; open STDERR, ">&BUILD" or croak "cannot reopen stderr: $!"; # prepare the dpkg-buildpackage command and environment my $cmd = "DEB_BUILD_OPTIONS='$dpkg_options' "; $cmd .= "DEB_VENDOR='$vendor' nice -n 7 dpkg-buildpackage "; $cmd .= "-a$arch $addsource -rfakeroot -d $dpkg_extra \n"; my $msg = "Building ${source} ${emvers} for ${arch} with $cmd"; print CYAN, wrap('','',$msg), RESET if ($verbose >= 2); # build the package here: my $exitval = 0; # comment out the next line for quick debugging. $exitval = system "$cmd"; $exitval = $exitval >> 8 if ($exitval > 0); # return things to normal before aborting with any dpkg errors close STDOUT; close STDERR; close BUILD; open STDOUT, ">&OLDOUT"; open STDERR, ">&OLDERR"; `rm -rf $crossbin` if ((defined $crossbin) and ($wraplibtool == 1)); system ("embug --failed") if ($exitval); exit ($exitval) if ($exitval > 0); exit (2) if ( not -f "../$changes"); chmod 0644, "${target_gnu_type}.cache" if (-f "${target_gnu_type}.cache"); # run the cross-built package through emgrip print GREEN, "Postprocessing cross-built $source for Emdebian Crush.\n", RESET; my $gripdir = "../${source}.grip"; (-d $gripdir) ? system ("rm -f $gripdir/*") : mkdir ($gripdir); my $test="emgrip -o $gripdir ../$changes"; my $env = "dpkg-architecture -a$arch -c"; print CYAN, "$env 'DEB_BUILD_OPTIONS=\"$vopts\" $test '\n", RESET if ($verbose >= 2); system ("$env 'DEB_BUILD_OPTIONS=\"$vopts\" $test '"); opendir (GRIP, $gripdir); my @gripped=grep(!/^\.\.?/, readdir(GRIP)); closedir(GRIP); foreach my $g (@gripped) { system ("cp $gripdir/$g ../$g") if ($g !~ /\.tar\.gz$/ and $g !~ /\.dsc$/); unlink ("$gripdir/$g"); } rmdir ($gripdir); # clean up the .gmo files system ("find . -name '*.gmo'|xargs rm -f"); my $allow = "--allow-root"; print GREEN, "Running lintian checks for Emdebian only.\n", RESET; system ("lintian $allow --color auto -ioC em ../$changes"); if ($? != 0) { # the embug flag is removed manually when lintian, newer and patches are all fixed. system ("embug --lintian"); die "\n" ; } my $tdebchg = "${source}_${emvers}tdeb_$arch.changes"; # lintian errors in TDebs are not fatal. system ("lintian $allow --color auto -iC em ../$tdebchg") if (-f "../$tdebchg"); print GREEN, "Finished running lintian.\n", RESET; open (CHG, "../$changes") or die ("FAILED: unable to read ../$changes: $!\n"); open (BUILD, ">>../$build") or die ("Cannot append .changes to .build: $!\n"); print BUILD "\n"; my $dir = cwd; $dir .= "/../$changes"; print (GREEN, "Changes file: " . &Cwd::realpath($dir)."\n", RESET); while() { print $_; print BUILD $_; } print GREEN, "\nSuccessful build.\n", RESET; print GREEN, "Build directory: " . cwd . "/" , RESET, "\n\n"; print BUILD "\nSuccessful build.\n\n"; close BUILD; close CHG; chdir ("../"); system "debsign $changes" if ($dosign eq "true"); &handle_svn($build, $cachefile) if ($svnci eq "true"); chomp($changes); my $manual_check = "Please check the package manually before uploading. e.g.\n"; $manual_check .= "'debc -a $arch ../${changes}'\n'deb-gview ../${changes}'.\n"; print CYAN, $manual_check, RESET if ($verbose >= 2); exit 0; # needs to eventually use the svn perl bindings # and then be renamed. # Subroutine to handle commits and deletions of emdebian patch files for # both emdebuild and emsource. # # Patch files that have been deleted (svn status = !) are deleted # from SVN. New patch files are added. # # Unfortunately, ever time a patch file is updated, it will always # be deemed 'modified' by svn because diff outputs a timestamp as # part of the file. These changes do not need to be committed but # there is no reliable way of detecting these changes in isolation. sub handle_svn { my @patchfiles = glob ('emdebian-*-locale-*.patch'); my $locpatch = join (" ", @patchfiles); my $svndel = scalar @patchfiles; if ($svndel > 0) { print GREEN, "Removing $svndel emlocale patches\n", RESET if ($verbose >= 1); `svn -q revert ./$locpatch`; `svn -q remove ./$locpatch`; `svn -q ci -m "Automated removal of locale patches" ./$locpatch`; } # if machine_variant has been used, skip committing to SVN return if (-f "../machine-patch.stamp"); @patchfiles = (); # Include all remaining patch files. print CYAN, "Checking in emdebian patch files . . \n", RESET if ($verbose >= 1); @patchfiles = glob ('emdebian-*.patch'); my @debpatch = glob ('debian-patch*'); push @patchfiles, @debpatch; my @svnadds=(); my @svncommits=(); my @remd = `svn status . | grep !`; my @remlist=(); foreach my $f (@remd) { $f =~ s/ //g; $f =~ s/!//; chomp ($f); push @remlist, $f; } `svn -q remove @remlist` if @remlist; `svn -q ci -m "Automated removal" @remlist` if @remlist; my $status = `svn status . | grep ?`; foreach my $f (@patchfiles) { push (@svnadds, $f) if $status =~ /\Q$f\E/; } $status = `svn status .`; foreach my $f (@patchfiles) { push (@svncommits, $f) if $status =~ /\Q$f\E/; } my $addnum = scalar @svnadds; my $commitnum = scalar @svncommits; if (@svnadds) { print GREEN, "Adding $addnum patch files to SVN . . \n", RESET if ($verbose == 1); print GREEN, "Adding patch files: ", @svnadds, "\n", RESET if ($verbose >= 2); my $add = join(' ', @svnadds); system ("svn -q add $add"); } if (@svncommits) { print GREEN, "Commit $commitnum patch files into SVN . . \n", RESET if ($verbose == 1); print GREEN, "Committing patch files: ", join(' ', @svncommits), "\n", RESET if ($verbose >= 2); system ("svn ci -q -m \"automated patch file checkin\" @svncommits"); } } sub crossbuilddeps { my @line = (); my @tools = (); print CYAN, "Checking for debian/control\n", RESET if ($verbose >= 2); &check_emdebian_control; print CYAN, wrap('','',"Checking for buildtime cross dependencies ". "in 'debian/xcontrol'.\n"), RESET if ($verbose >= 2); unless (-f "debian/xcontrol") { print CYAN, "No xcontrol file. Nothing to do.\n", RESET if ($verbose >= 1); return; } open (XC, "; close (XC); # allow multi-lines my $str = join ("", @xc); $str =~ s/\n / /g; $str =~ s/ / /g; my @long = split ("\n", $str); my @unmet = (); foreach my $dep (@long) { # allow xcontrol to contain more than just what we need. # support the old format until debian-xcontrol can handle # Homepage and Uploaders etc. if ($dep =~ /^Build-Cross-Depends: /) { $dep =~ s/^Build-Cross-Depends: //i; } elsif ($dep =~ /^Build-Depends: /) { $dep =~ s/^Build-Depends: //i; } # Could handle Build-Conflicts but not supported, yet, # see dpkg-checkbuilddeps.pl in dpkg : $dep->get_evaluation($facts). # Support Build-Depends-Tools: via apt-get. elsif ($dep =~ /^Build-Depends-Tools: /) { $dep =~ s/^Build-Depends-Tools: //i; # assume nothing is installed, ever. (apt-get will check that.) my $facts = Dpkg::Deps::KnownFacts->new(); my $dep_list = Dpkg::Deps::parse($dep, reduce_arch => 0); $dep_list->simplify_deps($facts); unless ($dep_list->is_empty()) { @unmet = $dep_list->get_deps(); } foreach my $depref (@unmet) { # handle OR architecture lists (may be incomplete) if ($depref->isa('Dpkg::Deps::OR')) { my $or = $$depref{'list'}; next if not defined ($or); my $ordep=""; foreach my $orpkg (@$or) { my $rel = $$orpkg{'package'}; if ($rel =~ /\Qnot\E/) { $rel =~ s/\Qnot+\E//; # $ordep contains the alternative - skip it if not for us. next if $host !~ /$rel/; push @line, $ordep; } $ordep .= " $rel"; } push @tools, $ordep; } next unless (defined $$depref{'package'}); push @tools, $$depref{'package'}; } my $depstr = join(" ", sort @tools); my $v = ""; $v = " -v" if ($verbose >= 2); $v = " -q" if ($verbose < 1); print GREEN, wrap('','',"Checking installation of build tools: ". "$depstr with apt-get${v} -y.\n"), RESET if ($verbose >= 1); print CYAN, "Enter your sudo password if prompted.\n", RESET if ($verbose >= 2); system ("sudo apt-get${v} -y install $depstr"); next; } else { next; } # assume nothing is installed, ever. (apt-cross will check that.) my $facts = Dpkg::Deps::KnownFacts->new(); my $dep_list = Dpkg::Deps::parse($dep, reduce_arch => 0); $dep_list->simplify_deps($facts); unless ($dep_list->is_empty()) { @unmet = $dep_list->get_deps(); } foreach my $depref (@unmet) { # handle OR architecture lists (may be incomplete) if ($depref->isa('Dpkg::Deps::OR')) { my $or = $$depref{'list'}; next if not defined ($or); my $ordep=""; foreach my $orpkg (@$or) { my $rel = $$orpkg{'package'}; if ($rel =~ /\Qnot\E/) { $rel =~ s/\Qnot+\E//; # $ordep contains the alternative - skip it if not for us. next if $target_gnu_type !~ /$rel/; next if $arch !~ /$rel/; push @line, $ordep; } $ordep .= " $rel"; } push @line, $ordep; } next unless (defined $$depref{'package'}); push @line, $$depref{'package'}; } } return unless (@line); return if ($host eq $arch); my $depstr = join(" ", sort @line); my $v = ""; $v = " -v" if ($verbose >= 2); $v = " -q" if ($verbose < 1); my $val = &get_primary; my $mirror = ""; $mirror = " -m ftp://$val/debian" if (defined $val); print GREEN, wrap('','',"Installing: $depstr with apt-cross${v}${suite}${mirror}.\n"), RESET if ($verbose >= 1); print CYAN, "Enter your sudo password if prompted.\n", RESET if ($verbose >= 2); system ("apt-cross -a $arch $v $suite $mirror -i $depstr"); # clean up in case some .debs are left over. opendir (DEBS, ".") or return; my @files=grep(/^.*\.deb$/,readdir DEBS); closedir (DEBS); foreach my $debf (@files) { unlink $debf; } }