The R Project SVN R

Rev

Rev 51393 | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed

#! @PERL@
#-*- perl -*-

## Copyright (C) 2000-2008 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 at
## http://www.r-project.org/Licenses/

## 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::Utils;
use Text::Wrap;

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

my $revision = ' $Rev: 52100 $ ';
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-data", "force", "no-vignettes");

if($WINDOWS) {
    die "Please set TMPDIR to a valid temporary directory\n"
    unless (-d ${R::Vars::TMPDIR});
    @known_options = ("help|h", "version|v", "binary", "no-docs",
              "auto-zip", "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");
## The tar.exe in Rtools has --force-local by default, but this
## enables people to use Cygwin or MSYS tar.
my $tar_default = "tar";
$tar_default = "tar --force-local" if $WINDOWS;
my $tar = R_getenv("TAR", $tar_default);
my $libdir = R_tempfile("Rinst");

my $INSTALL_opts = "";
$INSTALL_opts .= " --use-zip-data" if $opt_use_zip_data;
$INSTALL_opts .= " --no-docs" if $opt_no_docs;
if($WINDOWS) {
    $INSTALL_opts .= " --auto-zip" if $opt_auto_zip;
}

## <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) {
    $pkg =~ s/\/$//;
    ## Older versions used $pkg as absolute or relative to $startdir.
    ## This does not easily work if $pkg is a symbolic link.
    ## Hence, we now convert to absolute paths.
    chdir($startdir);
    chdir($pkg) or die "Error: cannot change to directory '$pkg'\n";
    ## (We could be nicer ...)
    my $pkgdir = R_cwd();
    my $pkgname = basename($pkgdir);
    my $intname;
    ## chdir($startdir);
    ## (Does not hurt, but should no longer be necessary.)

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

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

    if($description->{"Bundle"}) {
    $log->message("looks like '${pkgname}' is a package bundle -- they are defunct");
    $log->error();
    exit(1);
    }

    $log->message("preparing '$pkg':");
    $intname = $description->{"Package"};
    prepare_pkg($pkgdir, $description, $log);

    ## chdir($startdir);

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

    chdir(dirname($pkgdir));
    my $filename = "${intname}_" . $description->{"Version"} . ".tar";
    my $filepath = &file_path($startdir, $filename);
    R_system(join(" ",
                 ("$tar -chf",
                  &shell_quote_file_path($filepath),
                  "$pkgname")));
    my $tmpdir = R_tempfile("Rbuild");
    rmtree($tmpdir) if(-d $tmpdir);
    mkdir("$tmpdir", 0755)
       or die "Error: cannot create directory '$tmpdir'\n";
    chdir($tmpdir);
    ## was xhf, but there are no symbolic links here and that is invalid
    ## on FreeBSD, see http://www.freebsd.org/cgi/man.cgi?query=tar&apropos=0&sektion=0&manpath=FreeBSD+5.4-RELEASE+and+Ports&format=html
    R_system(join(" ",
                 ("$tar -xf",
                  &shell_quote_file_path($filepath))));

    my $exclude = R_tempfile("Rbuild-exclude");
    open(EXCLUDE, "> $exclude")
    or die "Error: cannot open file '$exclude' for writing\n";
    binmode EXCLUDE if $WINDOWS;
    if(-f &file_path($pkgdir, ".Rbuildignore")) {
    open(RBUILDIGNORE, &file_path($pkgdir, ".Rbuildignore"));
    while(<RBUILDIGNORE>) {
        chomp;
        s/\r$//;  # careless people get Windows files on other OSes
        push(@exclude_patterns, $_) if $_;
    }
    close(RBUILDIGNORE);
    }
    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(-d $_ && /^CVS$/);
    print EXCLUDE "$File::Find::name\n" if(-d $_ && /^\.svn$/);
    print EXCLUDE "$File::Find::name\n" if(-d $_ && /^\.arch-ids$/);
    print EXCLUDE "$File::Find::name\n" if(-d $_ && /^\.bzr$/);
    print EXCLUDE "$File::Find::name\n" if(-d $_ && /^\.git$/);
    print EXCLUDE "$File::Find::name\n" if(-d $_ && /^\.hg$/);
    ## some authors managed to create ..Rcheck as well as pkgname.Rcheck
    print EXCLUDE "$File::Find::name\n" if(-d $_ && /\.Rcheck$/);
    ## some authors have problems with 'Read-and-delete-me'
    print EXCLUDE "$File::Find::name\n" if /^Read-and-delete-me$/;
    print EXCLUDE "$File::Find::name\n" if /^GNUMakefile$/;
    ## Mac resource forks
    print EXCLUDE "$File::Find::name\n" if(/^\._/);
    ## Mac Finder files
    print EXCLUDE "$File::Find::name\n" if(/^\.DS_Store$/);
    ## Windows DLL resource file
    push(@exclude_patterns, "^src/" . $pkgname . "_res\\.rc");
    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";
        }
        }
    }
    }
    find(\&find_exclude_files, "$pkgname");
    close(EXCLUDE);

    ## Remove exclude files.
    open(EXCLUDE, "< $exclude");
    while(<EXCLUDE>) {
    rmtree(glob($_));
    }
    close(EXCLUDE);
    unlink($exclude);
    unlink($filepath);
    ## Now correct the package name (PR#9266)
    if($pkgname ne $intname) {
    rename $pkgname, $intname
        or die "Error: cannot rename directory to '$intname'\n";
    $pkgname = $intname;
    }
    ## Fix up man, R, demo inst/doc directories
    sub find_invalid_files {
    my ($dpath) = @_;
    my $Rcmd = "tools:::.check_package_subdirs(\"$dpath\", TRUE)\n";
    ## We don't run this in the C locale, as we only require
    ## certain filenames to start with ASCII letters/digits, and not
    ## to be entirely ASCII.
    my @out = R_runR($Rcmd, "--vanilla --slave",
             "R_DEFAULT_PACKAGES=NULL");
    @out = grep(!/^\>/, @out);
    if(scalar(@out) > 0) {
        $log->message("excluding invalid files from '$dpath'");
        $log->print(join("\n", @out) . "\n");
    }
    }
    &find_invalid_files($pkgname);
    chdir($tmpdir);

    ## 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 and make files");
    &fix_nonLF_in_source_files($pkgname, $log);
    &fix_nonLF_in_make_files($pkgname, $log);

    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) {
        $log->print("WARNING: directory '$File::Find::name' is empty\n");
        }
    }
    }

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

    sub fix_unneeded_dirs {
    my ($path) = @_;
    my $dir;
    foreach my $system_dir ("Meta", "R-ex", "chtml",
               "help", "html", "latex") {
        $dir = &file_path($path, $system_dir);
        if(-d "$dir") {
        $log->print(wrap("", "  ",
                 ("WARNING: Removing directory '$dir'",
                  "which should only occur",
                  "in an installed package\n")
                 ));
        rmtree("$dir");
        }
    }
    }

    &fix_unneeded_dirs($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) {
        $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"); }
    } 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 {
    ## precaution for Mac OS X to omit resource forks
    $ENV{COPYFILE_DISABLE} = 1; # Leopard
    $ENV{COPY_EXTENDED_ATTRIBUTES_DISABLE} = 1; # Tiger
    $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($filepath))));
    }
    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");
    my @dt = R_runR("cat(format(Sys.time(), '', tz='UTC', usetz=TRUE))",
            "--vanilla --slave", "R_DEFAULT_PACKAGES=NULL");
    $fh->print("Packaged: ", @dt, "; ", $user_name, "\n");
    $fh->close();
}

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

    my $pkgname = basename($pkgdir);

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

    &cleanup_pkg($pkgdir, $log);

    ## 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;
        $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($pkgdir)));
        } else {
        $cmd = join(" ",
                (&shell_quote_file_path(${R::Vars::R_EXE}),
                 "CMD INSTALL -l",
                 &shell_quote_file_path($libdir),
                 &shell_quote_file_path($pkgdir)));
        }
        if($doit && R_system($cmd)) {
        $log->error();
        $log->print("Installation failed.\n");
        $log->print("Removing '$libdir'\n");
        rmtree($libdir);
        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 %result = R_run_R($Rcmd, "--vanilla --no-save --quiet");
        rmtree("$libdir");
        $ENV{'R_LIBS'} = $R_LIBS;
        if($result{"status"}) {
        my @out = grep(!/^\>/, @{$result{"out"}});
        $log->error();
        $log->print(join("\n", @out) . "\n");
        exit(1);
        }
        else {
        $log->result("OK");
        }

        ## And finally, clean up again.
        &cleanup_pkg($pkgdir, $log);
    }
    }

    1;
}

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

    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") {
        ## FIXME: why not use 'Makefile' if Makefile.win does not exist?
        R_system("${R::Vars::MAKE} -f Makefile.win clean");
        } else {
        if(-r "Makevars.win") {
            my $makefiles = " -f " .
            &shell_quote_file_path(&file_path(${R::Vars::R_SHARE_DIR},
                              "make", "clean.mk"));
            $makefiles .= " -f Makevars.win";
            R_system("${R::Vars::MAKE} $makefiles clean");
        } elsif (-r "Makevars") {
            my $makefiles = " -f " .
            &shell_quote_file_path(&file_path(${R::Vars::R_SHARE_DIR},
                              "make", "clean.mk"));
            $makefiles .= " -f Makevars";
            R_system("${R::Vars::MAKE} $makefiles clean");
        }
        foreach my $file (<*.o $pkgname.a $pkgname.dll $pkgname.def>) {
            unlink($file);
        }
        rmtree("_libs") if (-d "_libs");
        }
    } else {
        my $makefiles = "-f " .
        &shell_quote_file_path(&file_path(${R::Vars::R_HOME},
                          "etc".$ENV{"R_ARCH"},
                          "Makeconf"));
        if(-r "Makefile") {
        $makefiles .= " -f Makefile";
        R_system("${R::Vars::MAKE} $makefiles clean");
        } else {
        if(-r "Makevars") {
            ## ensure we do have a 'clean' target.
            $makefiles .= " -f " .
            &shell_quote_file_path(&file_path(${R::Vars::R_SHARE_DIR},
                              "make", "clean.mk"));
            $makefiles .= " -f Makevars";
            R_system("${R::Vars::MAKE} $makefiles clean");
        }
        ## Also cleanup possible Windows leftovers ...
        unlink((<*.o *.s[lo] *.dylib>,
            "$pkgname.a", "$pkgname.dll", "$pkgname.def"));
        rmtree(".libs") if (-d ".libs");
        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_nonLF_in_source_files {
    my ($pkgname, $log) = @_;

    if(-d "$pkgname/src") {
    my @src_files = &list_files_with_type("$pkgname/src",
                          "src_no_CRLF");
    foreach my $file (@src_files) {
        my $has_nonLF = 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>) {
        chomp;
        $has_nonLF = 1 if $_ =~ /\r/;
        $_ =~ s/\r$//;
        # any remaining CRs are internal and so line endings.
        $_ =~ s/\r/\n/g;
        print TFILE "$_\n";
        }
        close(TFILE); close(FILE);
        if ($has_nonLF) {
        $log->print("    file '$file' had non-LF 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 fix_nonLF_in_make_files {
    my ($pkgname, $log) = @_;

    if(-d "$pkgname/src") {
    my $file;
    foreach my $f ("Makefile", "Makefile.in", "Makevars", "Makevars.in")  {
        $file = "$pkgname/src/$f";
        next unless -f "$file";
        my $has_nonLF = 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>) {
        chomp;
        $has_nonLF = 1 if $_ =~ /\r/;
        $_ =~ s/\r$//;
        # any remaining CRs are internal and so line endings.
        $_ =~ s/\r/\n/g;
        print TFILE "$_\n";
        }
        close(TFILE); close(FILE);
        if ($has_nonLF) {
        $log->print("    file '$file' had non-LF 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 <<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 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 "  --auto-zip            " .
        "select zipping of data based on size\n";
    }
    print <<END;
  --use-zip-data        collect data files in zip archive
  --no-docs             do not build and install documentation

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