#!/usr/bin/perl -w
# Copyright Eduard Bloch <blade@debian.org>, 2003, 2004, 2005, 2006
# License: GPLv2
# Version: $Id: svn-buildpackage 2210 2006-02-04 14:55:38Z inet $

use Getopt::Long qw(:config no_ignore_case bundling pass_through);
use File::Basename;
use Cwd;

use strict;
#use diagnostics;

my $startdir=getcwd;
chomp(my $tmpfile=`mktemp`);
my $scriptname="[svn-buildpackage]";

sub help {
print "
Usage: svn-buildpackage [ OPTIONS... ] [ OPTIONS for dpkg-buildpackage ]
Builds Debian package within the SVN repository. The source code
repository must be in the format created by svn-inject, and this script
must be executed from the work directory (trunk/package).

Building and working directory management:
  --svn-builder CMD    Use CMD as build command instead of dpkg-buildpackage
  --svn-ignore-new     Don't stop on svn conflicts or new/changed files
  --svn-dont-clean     Don't run debian/rules clean (default: clean first)
  --svn-savecfg        Create a .svn/deb-layout file from the detected/imported
                       layout information. (replicates old behaviour)
Source copying behavior:
  --svn-no-links       Don't use file links (default: use links where possible)
  --svn-dont-purge     Don't wipe the build directory (default: purge after build)
  --svn-reuse          Reuse an existing build directory, copy trunk over it
  --svn-rm-prev-dir    Remove an existing build directory instead of making a
                       copy; if --svn-reuse is specified, this option is reset
  --svn-export         Just prepares the build directory and exits
Tagging and post-tagging:
  --svn-tag            Final build: Export && build && tag && dch -i
  --svn-retag          Replace an existing tag directory if found while tagging
  --svn-only-tag       Tags the current trunk directory without building
  --svn-noautodch      Don't add a new Debian changelog entry when done
Post-build processing:
  --svn-lintian        Run lintian after the build
  --svn-linda          Like --svn-lintian, run linda instead
  --svn-move           Move package files to .. after successful build
  --svn-move-to XYZ    Move package files to XYZ, implies --svn-move
Miscelaneous:
  --svn-pkg PACKAGE    Specifies the package name
  --svn-override a=b   Override some config variable (comma separated list)
  --svn-verbose        More verbose program output
  --svn-noninteractive Turn off interactive mode
  -h, --help           Show this help message

If the debian directory has the mergeWithUpstream property, svn-buildpackage
will extract .orig.tar.gz file first and add the Debian files to it.

"; exit 1;
}
my $quiet="-q";
my $opt_verbose;
my $opt_dontclean;
my $opt_dontpurge;
my $opt_reuse;
my $opt_rmprevbuilddir;
my $opt_ignnew;
my $opt_tag;
my $opt_only_tag;
my $opt_lintian;
my $opt_linda;
my $opt_nolinks;
my $opt_noninteractive;
my $opt_pretag;
my $opt_prebuild;
my $opt_posttag;
my $opt_postbuild;
my $opt_retag;
my $opt_buildcmd;
my $opt_export;
my $opt_pass_diff;
my @opt_override;
my $opt_move;
my $opt_move_to;
my $opt_noautodch;
my $package;
my $opt_savecfg;
my $opt_dbgsdcommon;

my %options = (
#   "h|help"                => \&help,
   "svn-savecfg"           => \$opt_savecfg,
   "svn-verbose"           => \$opt_verbose,
   "svn-ignore-new|svn-ignore"        => \$opt_ignnew,
   "svn-dont-clean"        => \$opt_dontclean,
   "svn-export"            => \$opt_export,
   "svn-dont-purge"        => \$opt_dontpurge,
   "svn-reuse"             => \$opt_reuse,
   "svn-rm-prev-dir"       => \$opt_rmprevbuilddir,
   "svn-only-tag"          => \$opt_only_tag,
   "svn-tag-only"          => \$opt_only_tag,
   "svn-tag"               => \$opt_tag,
   "svn-retag"             => \$opt_retag,
   "svn-lintian"           => \$opt_lintian,
   "svn-linda"             => \$opt_linda,
   "svn-no-links"          => \$opt_nolinks,
   "svn-noninteractive"    => \$opt_noninteractive,
   "svn-pass-diff"         => \$opt_pass_diff,
   "svn-prebuild=s"        => \$opt_prebuild,
   "svn-postbuild=s"       => \$opt_postbuild,
   "svn-pretag=s"          => \$opt_pretag,
   "svn-posttag=s"         => \$opt_posttag,
   # and for compatibility with old config directives
   "pre-tag-action=s"      => \$opt_pretag,
   "post-tag-action=s"     => \$opt_posttag,
   "pre-build-action=s"    => \$opt_prebuild,
   "post-build-action=s"   => \$opt_postbuild,
   "svn-move"              => \$opt_move,
   "svn-move-to=s"         => \$opt_move_to,
   "svn-builder=s"         => \$opt_buildcmd,
   "svn-override=s"        => \@opt_override,
   "svn-pkg=s"             => \$package,
   "svn-noautodch"         => \$opt_noautodch,
   "svn-dbgsdcommon"       => \$opt_dbgsdcommon
);

if (! defined $opt_dbgsdcommon) {
    use lib "/usr/share/svn-buildpackage" ;
} else {
    use lib ".";
} ;

use SDCommon;

my $tagVersion;
my $upVersion;
my $tagVersionNonEpoch;
my @builder;
my $origExpect;
my $origfile;
my @dirs;
my @files;
my $retval=0;
my $orig_target;

# get only --help here, do before init because init needs to run after the main Getopt call, but may fail and ignore --help
#
{
   my $gethelp;
   &help unless GetOptions('h|help' => \$gethelp);
   &help if $gethelp;
}

SDCommon::init();

sub setenv {
   my ($key, $val) = @_;
   return 0 if(!defined($val));
   print "ENV: $key=$val\n" if $opt_verbose;
   $ENV{$key}=$val;
}

sub setallenv {
   $tagVersion=$SDCommon::tagVersion;
   $upVersion=$SDCommon::upVersion;
   $tagVersionNonEpoch = $tagVersion;
   $tagVersionNonEpoch =~ s/^[^:]*://;

   #this sucks but the config file needs to be processed before the options and there should be reasonable default
   $package = $SDCommon::package?$SDCommon::package:'' unless $package;
   setenv("PACKAGE", $package);
   setenv("package", $package);
   setenv "TAG_VERSION", $tagVersion;
   setenv "debian_version", $tagVersion;
   setenv "non_epoch_version", $tagVersionNonEpoch;
   setenv "upstream_version", $upVersion;
   setenv "SVN_BUILDPACKAGE", $SDCommon::version;
   setenv "guess_loc", ( ($package=~/^(lib.)/)?$1:substr($package,0,1))."/$package"."_$upVersion.orig.tar.gz";
}

&setallenv;

my @CONFARGS;
for my $file ($ENV{"HOME"}."/.svn-buildpackage.conf", ".svn/svn-buildpackage.conf") {

    if(open(RC, $file)) {
        SKIP: while(<RC>) {
            chomp;
            next SKIP if /^#/;
            # drop leading spaces
            s/^\s+//;
            if(/^\w/) {
                # remove spaces between
                s/^(\S+)\s*=\s*/$1=/;
                # convert to options and push to args
                s/^/--/;
                $_=`echo -n $_` if(/[\$`~]/);
                push(@CONFARGS, $_);
            }
        }
        close(RC);
    }
}

if($#CONFARGS>=0) {
   @ARGV=(@CONFARGS, @ARGV);
   print "Imported config directives:\n\t".join("\n\t", @CONFARGS)."\n";
}

&help unless ( GetOptions(%options));
$quiet="" if ($opt_verbose);
# if opt_only_tag is used, set opt_tag too. Should not hurt because the
# real function of opt_tag at the end of the script is never reached
$opt_tag = 1 if($opt_only_tag);
$opt_move=1 if $opt_move_to;
$opt_dontpurge=1 if $opt_reuse;
$opt_rmprevbuilddir=0 if $opt_reuse;
my $destdir=long_path($opt_move_to ? $opt_move_to : "$startdir/..");
$SDCommon::opt_verbose=$opt_verbose;
$package = $SDCommon::package if(!$package);
$SDCommon::opt_noninteractive = 1 if$opt_noninteractive;
$SDCommon::opt_noninteractive = 1 if exists($ENV{DEBIAN_FRONTEND}) && $ENV{DEBIAN_FRONTEND} =~ /^noninteractive$/;

$SDCommon::opt_nosave = 0 if (defined $opt_savecfg) ;

print "D: Configuration will not be saved.\n" if (defined $opt_verbose && ($SDCommon::opt_nosave=1));

SDCommon::configure;
needs_tagsUrl if($opt_tag);
my $c=\%SDCommon::c;

#some things may have been overriden by user options
&setallenv;
if($opt_buildcmd) { # pass @ARGV but carefully
   foreach(@ARGV) { s/"/\\"/g ; s/^/"/; s/$/"/;}
   @builder = ("/bin/sh", "-c", $opt_buildcmd." ".join(" ", @ARGV));
}
else {
   push(@builder, "dpkg-buildpackage",@ARGV);
   # a simple "helper". Only executed if:
   #  neither --svn-tag-only nor --evn-export is used and
   #  no custom command is choosen
   #  and no -d switch is there
   #  and no prebuild hook is set
   if(! $opt_only_tag && ! $opt_export && (!grep {$_ eq "-d"} @ARGV)
      && (! withechoNoPrompt("dpkg-checkbuilddeps")) 
      && ! $opt_prebuild )
   {
         die "Insufficient Build-Deps, stop!\n";
   }
}

withecho "fakeroot debian/rules clean || debian/rules clean" if ! ($opt_dontclean || (`svn proplist debian` =~ /mergeWithUpstream/i));
SDCommon::check_uncommited if(!$opt_ignnew);

my $parsechangelogret = `dpkg-parsechangelog`;
if (($? >> 8)!=0) {
  die "Failed to parse changelog";
}
if($parsechangelogret =~ /(\*\s+NOT.RELEASED.YET)|(UNRELEASED.*urgency)/m) {
   print STDERR "NOT RELEASED YET tag found - you don't want to release it with it, do you?\n";
   die "Aborting now, set \$FORCETAG to ignore it.\n" if($opt_tag && !$ENV{"FORCETAG"});
}

@opt_override = split(/,|\ |\r|\n/,join(',',@opt_override));
for(@opt_override) {
   $SDCommon::opt_nosave=1;
   if(/(.*)=(.*)/) {
      print "Overriding variable: $1 with $2\n" if $opt_verbose;
      $$c{$1}=$2;
   }
   else {
      print "Warning, unable to parse the override string: $_\n";
   }
}

sub checktag {
   SDCommon::svnMkdirP ( $$c{"tagsUrl"} ) ;
   if(insvn($$c{"tagsUrl"}."/$tagVersion")) {
      if($opt_retag) {
         withecho ("svn", "-m", "$scriptname Removing old tag $package-$tagVersion", "rm", $$c{"tagsUrl"}."/$tagVersion");
      }
      else {
         die "Could not create tag copy\n".
         $$c{"tagsUrl"}."/$tagVersion - it
does already exist. Add the --svn-retag option to replace that tag.\n";
      }
   }
}

sub dchIfNeeded {
      if ($opt_noautodch) {
          print "\nI: Done! No pending changelog entry was created since it was not requested.\n";
      }
      else {
          withecho "dch", "-D", "UNRELEASED", "-i", "NOT RELEASED YET";
          print "\nI: Done! Created the next changelog entry, please commit later or revert.\n";
      }
}

for(keys %{$c}) {
   setenv $_, $$c{$_};
}

if($opt_only_tag) {
   checktag;
   chdir $$c{"trunkDir"};
   system "$opt_pretag" if($opt_pretag);
   withecho ("svn", "-m", "$scriptname Tagging $package ($tagVersion)", "cp", $$c{"trunkUrl"}, $$c{"tagsUrl"}."/$tagVersion");
   system "$opt_posttag" if($opt_posttag);
   dchIfNeeded;
   SDCommon::sd_exit 0;
}

print "D: ",$opt_prebuild if $opt_verbose;

system "$opt_prebuild" if($opt_prebuild);

$$c{"buildArea"}=long_path($startdir."/..")."/build-area" if(!$$c{"buildArea"});

mkdir $$c{"buildArea"} if (! -d $$c{"buildArea"});

my $orig = $package."_".$upVersion.".orig.tar.gz";

if ($$c{"origDir"}) {
   $origExpect = $$c{"origDir"}."/$orig"; # just for the messages
   if (-f $origExpect) {
      $origfile = long_path($origExpect); # for the actual operation
   }
   else {
      if ($$c{"origUrl"}) {
         my $oUrl = $$c{"origUrl"};
         print "Orig tarball not found (expected $origExpect), fetching from $oUrl...\n";
         mkdir -p $$c{"origDir"} if (! -d $$c{"origDir"});
         system "wget -O $origExpect $oUrl" ;
         $origfile = long_path($origExpect); # now the file should exist
      };
   }

}
else {
    $origExpect = "(location unknown, guessed: ../tarballs/$orig)";
};

my $ba=$$c{"buildArea"};
my $bdir="$ba/$package-$upVersion";

if($opt_rmprevbuilddir && -e "$bdir") {
   print STDERR "$bdir exists, removing it, as requested\n";
   system "rm -fr $bdir" ;
}

if(!$opt_reuse && -e "$bdir") {
   my $backupNr=rand;
   print STDERR "$bdir exists, renaming to $bdir.obsolete.$backupNr\n";
   rename("$bdir","$bdir.obsolete.$backupNr");
}

mkdir "$ba" if(! -d "$ba");

if(`svn proplist debian` =~ /mergeWithUpstream/i) {
   print "mergeWithUpstream mode detected, looking for $origExpect\n";
}

# gets the upstream branch out of svn into .orig directory
sub exportToOrigDir {
   # no upstream source export by default and never in mergeWithUpstream mode
   if((!$ENV{"FORCEEXPORT"}) || `svn proplist debian` =~ /mergeWithUpstream/i) {
      return 0;
   }
   needs_upsCurrentUrl;
   defined($$c{"upsCurrentUrl"}) || print STDERR "upsCurrentUrl not set and not located, expect problems...\n";
   withecho("rm", "-rf", "$bdir.orig");
   withecho "svn", "export",$$c{"upsCurrentUrl"},"$bdir.orig";
}

# bold-ifies the output
sub boldify {
   system ( "tput", "smso" ) if ( (defined $ENV{"TERM"}) && ($ENV{"TERM"}) ) ;
}
# unbold-ifies the output
sub unboldify {
   system ( "tput", "rmso" ) if ( (defined $ENV{"TERM"}) && ($ENV{"TERM"}) ) ;
}

# non-Debian-native package detected, needing some kind of upstream source for
# dpkg-buildpackage (most likely, spew error messages but continue on native
# packages with dashes)
if($tagVersion =~ /-/) {
   my $abs_origfile=long_path($origfile);
   $orig_target="$ba/".$orig;
   if($opt_verbose) {
      print "Trying different methods to export the upstream source:\n";
      print " - making hard or symbolic link from $origExpect\n" if (!$opt_nolinks);
      print " - copying the tarball to the expected destination file\n";
   }
   else {
      print "W: $abs_origfile not found, expect problems...\n" if(! -e $abs_origfile);
   }
   if($origfile && -e $abs_origfile) {
      if(-e $orig_target) {
         if(((stat($abs_origfile))[7]) != ((stat($orig_target))[7]))
         {
            die "$orig_target exists but differs from $abs_origfile!\nAborting, fix this manually...";
         }
      }
      else {
         # orig in tarball-dir but not in build-area
         if($opt_nolinks) {
            withechoNoPrompt("cp", long_path($origfile), "$ba/$orig")
            ||
            exportToOrigDir;
         }
         else {
            link(long_path($origfile),"$ba/".$orig)
            ||
            symlink(long_path($origfile),"$ba/".$orig)
            ||
            withechoNoPrompt("cp",long_path($origfile),"$ba/$orig")
            ||
            exportToOrigDir;
         }
      }
   }
   else {
      # no orig at all, try exporting
      exportToOrigDir;
   }
}

# contents examination for "cp -l" emulation
print STDERR "Creating file list...\n" if $opt_verbose;
my %tmp;
set_statusref(\%tmp);
my $ctx = new SVN::Client;
$ctx->status("", "BASE", \&SDCommon::collect_name, 1, 1, 0, 1);


# contents examination for "cp -l" emulation
print STDERR "Creating file list...\n" if $opt_verbose;
$ctx->status("", "BASE", \&SDCommon::collect_name, 1, 1, 0, 1);

for(keys %tmp) {
    s#/$##;
    next if !length($_);
    next if ($tmp{$_} eq 2 || $tmp{$_} eq 5 || $tmp{$_} eq 11); # skip junk and deleted files
    if(-d $_ and not -l $_ ) {
        push(@dirs,$_);
        print STDERR "DIR: $_\n" if $opt_verbose;
    }
    else {
	next if /^\s*$/;
        push(@files,$_);
        print STDERR "FILE: $_\n" if $opt_verbose;
    }
}

if(`svn proplist debian` =~ /mergeWithUpstream/i) {
   print STDERR "I: mergeWithUpstream property set, looking for upstream source tarball...\n";
   die "E: Could not find the origDir directory, please check the settings!\n" if(! -e $$c{"origDir"});
   die "E: Could not find the upstream source file! (should be $origExpect)\n" if(! ($origfile && -e $origfile));
   my $mod=rand;
   mkdir "$ba/tmp-$mod";
   if($opt_reuse && -d $bdir) {
      print "Reusing old build directory\n" if $opt_verbose;
   }
   else {
      withecho "tar", "zxf", $origfile, "-C", "$ba/tmp-$mod";
      my @entries = (<$ba/tmp-$mod/*>);
      if (@entries == 1) {
         # The files are stored in the archive under a top directory, we
         # presume
         withecho "mv", (<$ba/tmp-$mod/*>), $bdir;
      }
      else {
         # Otherwise, we put them into a new directory
         withecho "mv", "$ba/tmp-$mod", $bdir;
      }
   }
   if($opt_nolinks || $opt_ignnew) {
      withecho ("svn", "--force", "export", $$c{"trunkDir"},"$bdir");
   }
   else {
      mkdir $bdir;
      #fixme maybe rewrite to withecho
      if( !withechoNoPrompt("mkdir","-p", map { "$bdir/$_" } @dirs) || !withechoNoPrompt("cp", "--parents", "-laf", @files, $bdir))
      { # cp failed...
         withecho "svn", "--force", "export", $$c{"trunkDir"},"$bdir";
      }
   }
   withecho "rm", "-rf", "$ba/tmp-$mod";
}
else {
   if($opt_nolinks) {
      withecho "svn","--force", "export",$$c{"trunkDir"},"$bdir";
   }
   else {
      mkdir $bdir;
      # stupid autodevtools are confused but... why?
      #if(system("mkdir", map { "$bdir/$_" } sort(@dirs)) + system ("cp", "--parents", "-laf", @files, $bdir) )
      if( !withechoNoPrompt("mkdir","-p", map { "$bdir/$_" } @dirs) || !withechoNoPrompt("cp", "--parents", "-laf", @files, $bdir))
      #open(tpipe, "| tar c --no-recursion | tar --atime-preserve -x -f- -C $bdir");
      #for(@dirs) {print tpipe "$_\n"}
      #close(tpipe);
      #if(system ("cp", "--parents", "-laf", @files, $bdir))
      { # cp failed...
         system "rm", "-rf", $bdir;
         withecho "svn", "--force", "export",$$c{"trunkDir"},$bdir;
      }
   }
}

# a cludge...
my $afile;
my $bfile;
my $cfile;
if($opt_pass_diff) {
   my $dirname="$package-$upVersion";
   needs_upsCurrentUrl;

   if(`env LC_ALL=C svn status $$c{"trunkDir"}` =~ /(^|\n)(A|M|D)/m) {
      print STDERR "Warning, uncommited changes found, using combinediff to merge them...\n";
      chomp($afile=`mktemp`);
      chomp($bfile=`mktemp`);
      chomp($cfile=`mktemp`);
      withecho "svn diff ".$$c{"upsCurrentUrl"}." ".$$c{"trunkUrl"}." > $afile";
      withecho "cd ".$$c{"trunkDir"}." ; svn diff > $bfile";
      withecho "combinediff $afile $bfile > $cfile";
      open(DIFFIN, "cat $cfile |");
   }
   else {
      open(DIFFIN, "svn diff ".$$c{"upsCurrentUrl"}." ".$$c{"trunkUrl"}." |");
   }
   open(DIFFOUT,">$tmpfile");
   # fix some diff junk
   my $invalid=1;
   while(<DIFFIN>) {
      s!^--- (\S+).*!--- $dirname.orig/$1!;
      s!^\+\+\+ (\S+).*!+++ $dirname/$1!;
      $invalid=0 if(/^---/);
      $invalid=1 if( (!$invalid) && /^[^+\-\t\ @]/);
      $invalid || print DIFFOUT $_;
   }
   close(DIFFIN);
   close(DIFFOUT);
   $ENV{"DIFFSRC"}=$tmpfile;
}

chdir $bdir || die "Mh, something is going wrong with builddir $bdir...";

if($opt_export) { print "Build directory exported to $bdir\n"; exit 0;}

if (!withecho(@builder)) {
   system "$opt_postbuild" if($opt_postbuild);
   print STDERR "build command failed in $bdir\nAborting.\n";
   print STDERR "W: build directory not purged!\n";
   print STDERR "W: no lintian/linda checks done!\n" if($opt_lintian);
   print STDERR "W: package not tagged!\n" if($opt_tag);
   SDCommon::sd_exit 1;
}
else {

    system "$opt_postbuild" if($opt_postbuild);

    chdir "..";

    if( ! $opt_postbuild) {
        my $chfile='';
        my @newfiles=();
        for my $arch ('source', `dpkg --print-architecture`) {
            next if ! $arch;
            my $file="$package"."_$tagVersionNonEpoch"."_$arch.changes";
            $file=~s/\r|\n//g; # something like chomp before does not work on constant values, 'source'
            push(@newfiles, "$ba/$file") if -e "$ba/$file";
            $chfile=$file if(-r "$ba/$file");
        }

        if(open(CH, "<$ba/$chfile")) {
            while(<CH>) { push(@newfiles, $1) if(/^\s\w+\s\d+\s\S+\s\w+\s(.+)\n/); }
            close(CH);

            if($opt_move) {
                $retval=!withechoNoPrompt("mv", @newfiles, $destdir);
                if (-e $orig_target) {
                    $retval=!withechoNoPrompt("mv", $orig_target, $destdir);
                    push(@newfiles, $orig_target);
                }
            }
            else { $destdir=$ba; }

            # expand the paths in the list and kick non-binary packages
	          my $multi=0;
            map { if(/\.deb$/){ $_=" $destdir/$_"; $multi++}else{$_=""}} @newfiles;

            &boldify;
            print STDERR "build command was successful; binaries are in $destdir/. ";
            print STDERR "The changes file is:\n $destdir/$chfile\n";
            &unboldify;
            print STDERR "Binary package", ($multi > 1 ? "s:\n" : ":\n"), @newfiles, "\n";

            &boldify;
            print STDERR
               "Warning: $package should have an orig tarball but it does not!\nExpected filename: $origExpect\n"
               if(($upVersion ne $tagVersion) && ($tagVersion =~/-1$/) && !-e "$destdir/$orig");
            &unboldify;
        }
        else
        {
            &boldify if ($opt_move);
            print STDERR "Could not read the .changes file: $ba/$chfile";
            print STDERR " and thus failed to move the resulting files." if ($opt_move);
            print STDERR "\n";
        }

        if($opt_lintian) {
            withecho "lintian", "$destdir/$chfile";
        }

        if($opt_linda) {
            withecho "linda", "$destdir/$chfile";
        }
    }

   if($opt_tag) {
      system "$opt_pretag" if($opt_pretag);
      checktag;
      withecho ("svn", "-m", "$scriptname Tagging $package ($tagVersion)", "cp", $$c{"trunkUrl"}, $$c{"tagsUrl"}."/$tagVersion");
      system "$opt_posttag" if($opt_posttag);
      chdir $$c{"trunkDir"};
      dchIfNeeded;
   }

   # cleanup
   if(!$opt_dontpurge) {
      withecho "rm", "-rf", $bdir if(length($tagVersion));
      for($tmpfile, $afile, $bfile, $cfile) {
         unlink $_ if(defined $_ and -e $_);
      }
   }
}
SDCommon::sd_exit 0+$retval;


