#! @PERL@
#-*- perl -*-
# Copyright (C) 2000-2004 R Development Core Team
#
# 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, 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.
#
# A copy of the GNU General Public License is available via WWW at
# http://www.gnu.org/copyleft/gpl.html.	 You can also obtain it by
# writing to the Free Software Foundation, Inc., 59 Temple Place,
# Suite 330, Boston, MA  02111-1307  USA.

# Send any bug reports to r-bugs@r-project.org

use Cwd;
use File::Basename;
use File::Compare;
use File::Find;
use File::Path;
use File::Copy;
use Getopt::Long;
use IO::File;
use R::Dcf;
use R::Logfile;
use R::Rd;
use R::Utils;
use R::Vars;
use Text::DelimMatch;
use Text::Wrap;

## Don't buffer output.
$| = 1;

my $revision = ' $Revision: 1.81 $ ';
my $version;
my $name;
$revision =~ / ([\d\.]*) /;
$version = $1;
($name = $0) =~ s|.*/||;

R::Vars::error("R_HOME", "R_EXE");

my $WINDOWS = ($R::Vars::OSTYPE eq "windows");

my @exclude_patterns = R::Utils::get_exclude_patterns();

my @known_options = ("help|h", "version|v", "binary", "no-docs",
		     "use-zip", "use-zip-help", "use-zip-data",
		     "force", "no-vignettes");

if($WINDOWS) {
    die "Please set TMPDIR to a valid temporary directory\n"
	unless (-e ${R::Vars::TMPDIR});
    @known_options = ("help|h", "version|v", "binary", "docs:s",
		      "auto-zip",
		      "use-zip", "use-zip-help", "use-zip-data",
		      "force", "no-vignettes");
}

GetOptions(@known_options) or usage();

R_version("R add-on package builder", $version) if $opt_version;
usage() if $opt_help;

## Use system default unless explicitly specified otherwise.
$ENV{"R_DEFAULT_PACKAGES"} = "";

my $startdir = R_cwd();
my $R_platform = R_getenv("R_PLATFORM", "unknown-binary");
my $gzip = R_getenv("R_GZIPCMD", "gzip");
my $tar = R_getenv("TAR", "tar");
my $libdir = &file_path(${R::Vars::TMPDIR}, "Rinst.$$");

my $INSTALL_opts = "";
$INSTALL_opts .= " --use-zip" if $opt_use_zip;
$INSTALL_opts .= " --use-zip-data" if $opt_use_zip_data;
$INSTALL_opts .= " --use-zip-help" if $opt_use_zip_help;
if($WINDOWS) {
    $INSTALL_opts .= " --docs=$opt_docs" if $opt_docs;
    $INSTALL_opts .= " --auto-zip" if $opt_auto_zip;
} else {
    $INSTALL_opts .= " --no-docs" if $opt_no_docs;
}
## <FIXME>
## Once we have a 'global' log file, use $log->warning() instead of just
## print().
if(!$opt_binary && $INSTALL_opts ne "") {
    print "** Options '$INSTALL_opts' only for '--binary' ignored\n";
}
## </FIXME>

## This is the main loop over all packages to be packaged.
foreach my $pkg (@ARGV) {
    my $is_bundle = 0;
    $pkg =~ s/\/$//;
    my $pkgname = basename($pkg);
    chdir($startdir);

    my $log = new R::Logfile();

    my $description;
    $log->checking("for file '$pkg/DESCRIPTION'");
    if(-r &file_path($pkg, "DESCRIPTION")) {
	$description = new R::Dcf(&file_path($pkg, "DESCRIPTION"));
	$log->result("OK");
    }
    else {
	$log->result("NO");
	exit(1);
    }

    my @bundlepkgs;
    if($description->{"Contains"}) {
	$log->message("looks like '${pkg}' is a package bundle");
	$is_bundle = 1;
	@bundlepkgs = split(/\s+/, $description->{"Contains"});
	foreach my $ppkg (@bundlepkgs) {
	    $log->message("cleaning '$ppkg' in bundle '$pkg'");
	    $log->setstars("**");
	    chdir($startdir);
	    cleanup_pkg(&file_path("$pkg", "$ppkg"), $log);
	    $log->setstars("*");
	}
	foreach my $ppkg (@bundlepkgs) {
	    $log->message("preparing '$ppkg' in bundle '$pkg':");
	    $log->setstars("**");
	    chdir($startdir);
	    prepare_pkg(&file_path("$pkg", "$ppkg"), $is_bundle,
			$description, $log);
	    $log->setstars("*");
	}
	foreach my $ppkg (@bundlepkgs) {
	    $log->message("cleaning '$ppkg' in bundle '$pkg'");
	    $log->setstars("**");
	    chdir($startdir);
	    cleanup_pkg(&file_path("$pkg", "$ppkg"), $log);
	    $log->setstars("*");
	}
	rmtree("$libdir") if (-d "$libdir");
    } else {
	$is_bundle = 0;
	chdir($startdir);
	$log->message("preparing '$pkg':");
	prepare_pkg("$pkg", $is_bundle, $description, $log);
    }

    chdir($startdir);

    $log->message("removing junk files");
    find(\&unlink_junk_files, $pkg);

    my $exclude = &file_path(${R::Vars::TMPDIR}, "Rbuild-exclude.$$");
    open(EXCLUDE, "> $exclude")
	or die "Error: cannot open file '$exclude' for writing\n";
    binmode EXCLUDE if $WINDOWS;
    ## <NOTE>
    ## For bundles, the .Rbuildignore mechanism is not consistent
    ## between build and check: the latter always works on a per
    ## package basis.
    if(-f &file_path($pkg, ".Rbuildignore")) {
	open(RBUILDIGNORE, &file_path($pkg, ".Rbuildignore"));
	while(<RBUILDIGNORE>) {
	    chop;
	    push(@exclude_patterns, $_) if $_;
	}
	close(RBUILDIGNORE);
    }
    ## </NOTE>
    sub find_exclude_files {
	print EXCLUDE "$File::Find::name\n" if(-d $_ && /^check$/);
	print EXCLUDE "$File::Find::name\n" if(-d $_ && /^chm$/);
	print EXCLUDE "$File::Find::name\n" if(-d $_ && /[Oo]ld$/);
	print EXCLUDE "$File::Find::name\n" if /^GNUMakefile$/;
	print EXCLUDE "$File::Find::name\n" if(-d $_ && /^CVS$/);
	print EXCLUDE "$File::Find::name\n" if(-d $_ && /^\.svn$/);
	if($WINDOWS) {
	    ## exclude stray dependency files
	    print EXCLUDE "$File::Find::name\n" if /\.d$/;
	    print EXCLUDE "$File::Find::name\n" if /^Makedeps$/;
	    ## and the DLL resource file
	    my $resname = $pkgname . "_res.rc";
	    print EXCLUDE "$File::Find::name\n" if /^$resname$/;
	}
	my $filename = $File::Find::name;
	$filename =~ s/^[^\/]*\///;
	foreach my $p (@exclude_patterns) {
	    if($WINDOWS) {
		## Argh: Windows is case-honoring but not
		## case-insensitive ...
		print EXCLUDE "$File::Find::name\n"
		    if($filename =~	/$p/i);
	    }
	    else {
		if($filename =~ /$p/) {
		    ## Seems that the Tar '-X' option uses exclude
		    ## *shell* patterns, where '*', '?', and '[...]'
		    ## are the usual shell wildcards and '\' escapes
		    ## them.  Hence we need to escape the wildcard
		    ## characters in file names.  On Windows, the
		    ## first two are invalid (and hence rejected by
		    ## R CMD check), and the last two do not need
		    ## escaping.
		    $filename = "$File::Find::name";
		    $filename =~ s/\[/\\\[/g;
		    $filename =~ s/\]/\\\]/g;
		    print EXCLUDE "$filename\n";
		}
	    }
	}
    }
    chdir(&file_path("$pkg", ".."));
    find(\&find_exclude_files, "$pkgname");
    close(EXCLUDE);

    my $filename = "${pkgname}_" . $description->{"Version"} . ".tar";
    my $filepath = &file_path($startdir, $filename);
    ## under Windows, need separate Cygwin and Windows versions of path.
    my $origfilepath = $filepath;
    if($WINDOWS) {
	## workaround for paths in Cygwin tar
	$filepath =~ s+^([A-Za-x]):+/cygdrive/\1+;
    }
    R_system(join(" ",
		  ("$tar chf",
		   &shell_quote_file_path($filepath),
		   "$pkgname")));
    my $tmpdir = &file_path(${R::Vars::TMPDIR}, "Rbuild.$$");
    rmtree($tmpdir) if(-d $tmpdir);
    mkdir("$tmpdir", 0755)
	or die "Error: cannot create directory '$tmpdir'\n";
    chdir($tmpdir);
    R_system(join(" ",
		  ("$tar xhf",
		   &shell_quote_file_path($filepath))));
    ## Remove exclude files.
    open(EXCLUDE, "< $exclude");
    while(<EXCLUDE>) {
	rmtree(glob($_));
    }
    close(EXCLUDE);
    unlink($exclude);
    unlink($origfilepath);
    ## Fix permissions.
    sub fix_permissions {
	## Note that when called via File::Find::find, $_ holds the
	## file name within the current directory.
	if(-d $_) {
	    ## Directories should really be mode 00755 if possible.
	    chmod(00755, $_);
	}
	elsif(-f $_) {
	    ## Files should be readable by everyone, and writable
	    ## only for user.  This leaves a bit of uncertainty
	    ## about the execute bits.
	    chmod(((stat $_)[2] | 00644) & 00755, $_);
	}
    }
    find(\&fix_permissions, "${pkgname}") if(!$WINDOWS);
    ## Add build stamp to the DESCRIPTION file.
    &add_build_stamp_to_description_file(&file_path($pkgname,
						    "DESCRIPTION"));
    $log->message("checking for LF line-endings in source files");
    if($is_bundle) {
	foreach my $ppkg (@bundlepkgs) {
	    &fix_CRLF_in_source_files(&file_path($pkgname, $ppkg));
	}
    } else {
	&fix_CRLF_in_source_files($pkgname);
    }

    sub empty_dir_check {
	if(-d $_) {
	    opendir(DIR, $_) or die "cannot open dir $File::Find::name: $!";
	    my @files = readdir(DIR);
	    closedir(DIR);
	    if (@files <= 2) {
		print "WARNING: directory $File::Find::name is empty\n";
		if($File::Find::name =~ m+^$pkgname(/|\\)(help|html|latex|libs)$+) {
		    print "  A directory named '$_' should only occur in an installed package\n";
		}
	    }
	}
    }

    $log->message("checking for empty directories");
    find(\&empty_dir_check, "${pkgname}");

    ## Finalize.
    if($opt_binary) {
	$log->message("building binary distribution");
	chdir($startdir);
	if (!-d "$libdir") {
	    mkdir("$libdir", 0755)
		or die "Error: cannot create directory '$libdir'\n";
	}
	my $srcdir = &file_path($tmpdir, $pkgname);
	my $cmd;
	if($WINDOWS) {
	    $log->warning("some HTML links may not be found");
	    $cmd = join(" ",
			("Rcmd.exe INSTALL -l",
			 &shell_quote_file_path($libdir),
			 "--build $INSTALL_opts",
			 &shell_quote_file_path($srcdir)));
	    if(R_system($cmd)) { $log->error("installation failed"); }
	} elsif($is_bundle) {
	    $binfilename = "${pkgname}_" . $description->{"Version"} .
		"_R_${R_platform}.tar";
	    my $filepath = &file_path($startdir, $binfilename);
	    $cmd = join(" ",
			(&shell_quote_file_path(${R::Vars::R_EXE}),
			 "CMD INSTALL -l",
			 &shell_quote_file_path($libdir),
			 "$INSTALL_opts",
			 &shell_quote_file_path($srcdir)));
	    if(R_system($cmd)) { $log->error("installation failed"); }
	    chdir("$libdir");
	    copy(&file_path($startdir, $pkg, "DESCRIPTION"),
		 "DESCRIPTION");
	    R_system(join(" ",
			  ("$tar chf ",
			   &shell_quote_file_path($filepath),
			   @bundlepkgs, "DESCRIPTION")));
	    R_system(join(" ",
			  ("$gzip -9f ",
			   &shell_quote_file_path($filepath))));
	    chdir($startdir);
	    $log->message("packaged bundle '$pkgname' as '$binfilename.gz'");
	} else {
	    $cmd = join(" ",
			(&shell_quote_file_path(${R::Vars::R_EXE}),
			 "CMD INSTALL -l",
			 &shell_quote_file_path($libdir),
			 "--build $INSTALL_opts",
			 &shell_quote_file_path($srcdir)));
	    if(R_system($cmd)) { $log->error("installation failed"); }
	}
    } else {
	$log->message("building '$filename.gz'");
	R_system(join(" ",
		      ("$tar chf",
		       &shell_quote_file_path($filepath),
		       "$pkgname")));
	R_system(join(" ",
		      ("$gzip -9f",
		       &shell_quote_file_path($origfilepath))));
    }
    chdir($startdir);
    rmtree($tmpdir);
    $log->close();
    print("\n");
}


sub add_build_stamp_to_description_file {
    my ($dpath) = @_;

    my @lines = &read_lines($dpath);
    @lines = grep(!/^\s*$/, @lines); # Remove blank lines.
    my $user_name;
    if($WINDOWS) {
	$user_name = Win32::LoginName();
    }
    else {
	$user_name = (getpwuid($<))[0];
    }
    my $fh = new IO::File($dpath, "w")
	or die "Error: cannot open file '$dpath' for writing\n";
    ## Do not keep previous build stamps.
    @lines = grep(!/^Packaged:/, @lines);
    $fh->print(join("\n", @lines), "\n");
    $fh->print("Packaged: ",
	       scalar(localtime()), "; ",
	       $user_name, "\n");
    $fh->close();
}


sub prepare_pkg {
    my ($pkg, $in_bundle, $description, $log) = @_;

    chdir($pkg);
    my $pkgdir = R_cwd();
    my $pkgname = basename($pkgdir);

    &R::Utils::check_package_description($pkgdir, $pkgname, $log,
					 $in_bundle, 0, 1);

    &cleanup_pkg($pkgdir, $log) if(!$in_bundle);

    ## Only update existing INDEX files.
    &update_Rd_index("INDEX", "man", $log) if(-f "INDEX");

    if((-d &file_path("inst", "doc"))
       && &list_files_with_type(&file_path("inst", "doc"),
				"vignette")) {
	if(!$opt_no_vignettes) {
	    my $doit = 1;
	    ## if we are in a bundle, need to install the whole bundle
	    ## once.
	    my $pkg_or_bundle_dir;
	    if ($in_bundle) {
		$pkg_or_bundle_dir = dirname($pkgdir);
		if(-d "$libdir") {
		    $doit = 0;
		} else {
		    $log->message("installing the *bundle* to re-build vignettes");
		mkdir("$libdir", 0755)
		    or die "Error: cannot create directory '$libdir'\n";
		}
	    } else {
		$pkg_or_bundle_dir = $pkgdir;
		$log->message("installing the package to re-build vignettes");
		mkdir("$libdir", 0755)
		    or die "Error: cannot create directory '$libdir'\n";
	    }
	    my $cmd;
	    if($WINDOWS) {
		$cmd = join(" ",
			    ("Rcmd.exe INSTALL -l",
			     &shell_quote_file_path($libdir),
			     &shell_quote_file_path($pkg_or_bundle_dir)));
	    } else {
		$cmd = join(" ",
			    (&shell_quote_file_path(${R::Vars::R_EXE}),
			     "CMD INSTALL -l",
			     &shell_quote_file_path($libdir),
			     &shell_quote_file_path($pkg_or_bundle_dir)));
	    }
	    if($doit && R_system($cmd)) {
		$log->error();
		$log->print("Installation failed.\n");
		exit(1);
	    }
	    my $R_LIBS = $ENV{'R_LIBS'};
	    $ENV{'R_LIBS'} = env_path("$libdir", $R_LIBS);

	    $log->creating("vignettes");
	    my $Rcmd = "library(tools)\n";
	    $Rcmd .= "buildVignettes(dir = '.')\n";
	    my @out = R_runR($Rcmd, "--vanilla --no-save --quiet");
	    rmtree("$libdir") unless $in_bundle;
	    $ENV{'R_LIBS'} = $R_LIBS;
	    my @err = grep(/^Error/, @out);
	    if(scalar(@err) > 0) {
		@out = grep(!/^\>/, @out);
		$log->error();
		$log->print(join("\n", @out) . "\n");
		exit(1);
	    }
	    else {
		$log->result("OK");
	    }

	    ## And finally, clean up again (if not in a bundle).
	    &cleanup_pkg($pkgdir, $log) if(!$in_bundle);
	}
    }

    1;
}

sub cleanup_pkg {
    my ($pkg, $log) = @_;

    chdir($pkg);
    my $pkgdir = R_cwd();
    my $pkgname = basename($pkgdir);    

    if(-d "src") {
	chdir("src");
	$log->message("cleaning src");
	if($WINDOWS) {
	    ## A Windows Makefile.win might use
	    ## $(RHOME)/src/gnuwin32/MkRules.
	    $ENV{RHOME} = $ENV{R_HOME};
	    if(-r "Makefile.win") {
		R_system("${R::Vars::MAKE} -f Makefile.win clean");
	    } else {
		foreach my $file (<*.o $pkgname.a $pkgname.dll $pkgname.def>) {
		    unlink($file);
		}
		rmtree("_libs") if (-d "_libs");
	    }
	} else {
	    if(-r "Makefile") {
		my $makefiles = "-f " .
		    &shell_quote_file_path(&file_path(${R::Vars::R_HOME},
						      "etc", "Makeconf"));
		$makefiles .= " -f Makefile";
		R_system("${R::Vars::MAKE} $makefiles clean");
	    } else {
		foreach my $file (<*.o *.s[lo] .dylib>) {
		    unlink($file);
		}
		rmtree(".libs") if (-d ".libs");
	    }
	}
    }
    
    chdir($pkgdir);
    if(!$WINDOWS && -x "./cleanup") {
	$log->message("running cleanup");
	R_system("./cleanup");
    }
    
    1;
}
    

sub unlink_junk_files {
    unlink($_) if /^(\.RData|\.Rhistory)$/;
    if(/^DESCRIPTION$/) {
	unlink($_) if (-f "DESCRIPTION.in");
    }
}


sub update_index {
    my ($oldindex, $newindex, $log) = @_;

    $log->checking("whether '$oldindex' is up-to-date");
    if(-r $oldindex) {
	if(compare($oldindex, $newindex) != 0) {
	    $log->result("NO");
	    if($opt_force) {
		$log->message("overwriting '${oldindex}' as " .
			      "'--force' was given");
		unlink($oldindex);
		rename($newindex, $oldindex);
	    }
	    else {
		$log->message("use '--force' to overwrite " .
			      "the existing '${oldindex}'");
		unlink($newindex);
	    }
	}
	else {
	    $log->result("OK");
	    unlink($newindex);
	}
    }
    else {
	$log->result("NO");
	$log->message("creating new '$oldindex'");
	unlink($oldindex);
	rename($newindex, $oldindex);
    }

    1;
}

sub update_Rd_index {
    my ($oldindex, $Rd_files, $log) = @_;

    my $newindex = ".Rbuildindex.$$";

    my $Rcmd = "Rdindex(\"${Rd_files}\", \"${newindex}\")\n";

    my %result =
	R_run_R($Rcmd, "--vanilla --quiet", "R_DEFAULT_PACKAGES=tools");
    if($result{"status"}) {
	## This is a bit silly ... but just in case this fails, we want
	## a decent error message.
	my @out = grep(!/^\>/, @{$result{"out"}});
	$log->message("computing Rd index");
	$log->error();
	$log->print(join("\n", @out) . "\n");
	exit(1);
    }

    update_index($oldindex, $newindex, $log);
    1;
}

sub fix_CRLF_in_source_files {
    my ($pkgname) = @_;

    if(-d "$pkgname/src") {
	my @src_files = &list_files_with_exts("$pkgname/src", "(c|h|f|cc|cpp)");
	foreach my $file (@src_files) {
	    my $has_CRLF = 0;
	    open(FILE, "< $file")
		or die "Error: cannot open '$file' for reading\n";
	    open(TFILE, "> $file.tmp")
		or die "Error: cannot open '$file.tmp' for writing\n";
            binmode(FILE); binmode(TFILE);	# for Windows
	    while(<FILE>) {
		chop;
		$has_CRLF = 1 if $_ =~ /\r$/;
		$_ =~ s/\r$//;
		print TFILE "$_\n";
	    }
	    close(TFILE); close(FILE);
	    if ($has_CRLF) {
		print("    file '$file' had CRLF line endings\n");
		unlink($file); # should not be necessary, but is on Windows
		rename("$file.tmp", $file)
		    or die "Error: cannot rename '$file.tmp'\n";
	    } else {
		unlink("$file.tmp");
	    }
	}
    }
}


sub usage {
    print STDERR <<END;
Usage: R CMD $name [options] pkgdirs

Build R packages from package sources in the directories specified by
pkgdirs.

Options:
  -h, --help		print short help message and exit
  -v, --version		print 'build' version info and exit

  --force               force overwriting of INDEX file
  --no-vignettes        do not rebuild package vignettes

  --binary              build pre-compiled binary packages, with options:
END
    if($WINDOWS) {
	print STDERR "  --docs=TYPE           " .
	    "type(s) of documentation to build and install\n";
	print STDERR "  --auto-zip            " .
	    "select zipping of data and help based on size\n";
    } else {
	print STDERR "  --no-docs             " .
	    "do not build and install documentation\n";
    }
    print STDERR <<END;
  --use-zip-data        collect data files in zip archive
  --use-zip-help        collect help and examples into zip archives
  --use-zip             combine '--use-zip-data' and '--use-zip-help'

Email bug reports to <r-bugs\@r-project.org>.
END
    exit 0;
}