/*BINFMTC: parameter.c forkexec.c ilistcreate.c main.c
 *  cowbuilder / pbuilder with cowdancer
 *  Copyright (C) 2005-2009 Junichi Uekawa
 *
 *  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 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, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 *
 */
/*
  sudo ./cowbuilder --create --distribution sid --basepath /mnt/120g-1/dancer/cow/cow
  sudo ./cowbuilder --update --basepath /mnt/120g-1/dancer/cow/cow

  sudo mkdir /mnt/120g-1/dancer/cow/cow
  cd /mnt/120g-1/dancer/cow/cow
  sudo tar xfzp /var/cache/pbuilder/base.tgz
  sudo chroot . dpkg -i tmp/cowdancer_0.3_i386.deb

  sudo ./cowbuilder.c --build --basepath /mnt/120g-1/dancer/cow/cow $FILENAME

i.e. requires cowdancer inside the chroot.



How to find out changed times of chroot:
$ sudo find /var/cache/pbuilder/build/cow -mtime -1 -ls
will show the list of files changed.

 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <unistd.h>

#include "cowbuilder_util.h"
#include "ilist.h"
#include "parameter.h"

const char* ilist_PRGNAME="cowbuilder";


/**
   remove a directory.

   be careful about bind-mounted directories.

   @return 0 on success
 */
static int rmrf(const char* path)
{
  return forkexeclp("rm", "rm", "-rf", path, NULL);
}

/**
   Remove a directory, but only if nothing is mounted under it.

   @return 0 on success
 */
static int rmrf_check_mount(const char* path)
{
  /*
   * A directory bind-mounted into path will be cleaned out by rmrf().
   * To avoid that potential disaster we want to detect if there's
   * something mounted under path before deleting it.
   */

  char* canonical_path = strdupa(path);

  canonicalize_doubleslash(path, canonical_path);
  if (check_mountpoint(canonical_path)) {
    return 1;
  }

  return rmrf(path);
}

/**
 * @return .ilist path as malloc()ed string, or NULL on error.
 */
static char* get_ilistfile_path(const struct pbuilderconfig* pc)
{
  char *ilistfile;
  if (0>asprintf(&ilistfile, "%s/.ilist", pc->buildplace))
    {
      /* outofmemory */
      log_printf(log_error, "cowdancer: out of memory.");
      return NULL;
    }
  return ilistfile;
}

/**
   break hardlink.

   return 0 on success, 1 on fail.
 */
int break_cowlink(const char* s)
{
  char *backup_file;
  struct stat buf;
  int pid;
  int status;

  if(lstat(s, &buf))
    return 1;			/* if stat fails, the file probably
				   doesn't exist; return, success */
  if (S_ISLNK(buf.st_mode))
    return 1;			/* ignore symbollic link */

  if((buf.st_nlink > 1) && 	/* it is hardlinked */
     S_ISREG(buf.st_mode))
    {
      if (asprintf(&backup_file, "%sXXXXXX", s)==-1)
	{
	  ilist_outofmemory("out of memory in break_cowlink");
	  return 1;
	}
      switch(pid=fork())
	{
	case 0:
	  /* child process, run cp */
	  putenv("COWDANCER_IGNORE=yes");
	  execl("/bin/cp", "/bin/cp", "-a", s, backup_file, NULL);
	  log_perror("execl:cp:");
	  exit(EXIT_FAILURE);
	case -1:
	  /* error condition in fork(); something is really wrong */
	  ilist_outofmemory("out of memory in check_inode_and_copy, 2");
	  break;
	default:
	  /* parent process, waiting for cp -a to terminate */
	  if(-1==waitpid(pid, &status, 0))
	    {
	      log_perror("waitpid:cp");
	      log_printf(log_error, "%s: unexpected waitpid error when waiting for process %i with status %x",
		      ilist_PRGNAME, pid, status);
	      goto error_spm;
	    }

	  if (!WIFEXITED(status))
	    {
	      /* something unexpected */
	      log_printf(log_error, "%s: unexpected WIFEXITED status in waitpid: %x", ilist_PRGNAME,
		      (int)status);
	      goto error_spm;
	    }
	  else if (WEXITSTATUS(status))
	    {
	      /* cp -a failed */
	      log_printf(log_error, "%s: cp -a failed for %s", ilist_PRGNAME, backup_file);
	      goto error_spm;
	    }
	  /* when cp -a succeeded, overwrite the target file from the temporary file with rename */
	  else if (-1==rename(backup_file, s))
	    {
	      log_perror("file overwrite with rename");
	      log_printf(log_error, "while trying rename of %s to %s",  backup_file, s);
	      goto error_spm;
	    }
	}
    }
  return 0;

 error_spm:
  free(backup_file);
  return 1;
}

/**
   Convenience function to call break_cowlink with basepath prepended.
 */
static void break_cowlink_inside_chroot(const char* basepath, const char* filepath)
{
  char* filename;
  asprintf(&filename, "%s%s", basepath, filepath);
  break_cowlink(filename);
  free(filename);
}

/**
   hardlink-copy directory for COW use.  Un-hardlink some files which
   do not work with hardlinking.

   @return 0 on success, 1 on failure.
 */
static int cpbuilder_internal_cowcopy(const struct pbuilderconfig* pc)
{
  char *ilistfile;

  log_printf(log_info, "Copying COW directory");
  if (0!=rmrf_check_mount(pc->buildplace))
    return 1;
  if (0!=forkexeclp("cp", "cp", "-al", pc->basepath, pc->buildplace, NULL))
    return 1;

  /* ilist file handling.
     Delete existing ilist file if it exists, because I use COWDANCER_REUSE */
  if(!(ilistfile=get_ilistfile_path(pc)))
    {
      /* outofmemory */
      log_printf(log_error, "cowdancer: out of memory.");
      return 1;
    }
  if (unlink(ilistfile))
    {
      /* if there was no ilist file in the beginning, that's not a
	 problem.
       */
      log_printf(log_info, "unlink for ilistfile %s failed, it didn't exist?", ilistfile);
    }
  else
    {
      log_printf(log_info, "removed stale ilistfile %s", ilistfile);
    }
  free(ilistfile);

  /* Non-hardlinkable file support
   */
  break_cowlink_inside_chroot(pc->buildplace, "/var/cache/debconf/config.dat");
  break_cowlink_inside_chroot(pc->buildplace, "/var/cache/debconf/templates.dat");
  break_cowlink_inside_chroot(pc->buildplace, "/var/cache/debconf/passwords.dat");

  return 0;
}

/*
   @return 0 on success, 1 on failure.
 */
static int cpbuilder_internal_cleancow(const struct pbuilderconfig* pc)
{
  log_printf(log_info, "Cleaning COW directory");

  if (!pc->debug && 0!=rmrf_check_mount(pc->buildplace))
    return 1;
  return 0;
}

/*
 * @return: return code of pbuilder, or <0 on failure
 */
int cpbuilder_build(const struct pbuilderconfig* pc, const char* dscfile_)
{
  const char* dscfile=canonicalize_file_name(dscfile_);
  char* ilistfile;
  char* buf_chroot=NULL;
  int ret;
  char * prevdir;

  if (!dscfile)
    {
      log_printf(log_error, "File not found: %s", dscfile_);
      return 1;
    }

  if (cpbuilder_internal_cowcopy(pc))
    {
      log_printf(log_error, "Failed cowcopy.");
      return 1;
    }


  if (0>asprintf(&buf_chroot, "chroot %s cow-shell", pc->buildplace))
    {
      /* outofmemory */
      log_printf(log_error, "cowdancer: out of memory.");
      return -1;
    }

  if(!(ilistfile=get_ilistfile_path(pc)))
    {
      /* outofmemory */
      log_printf(log_error, "cowdancer: out of memory.");
      return 1;
    }

  prevdir=get_current_dir_name();
  chdir(pc->buildplace);

  if (forkexeclp("chroot",
		 "chroot",
		 pc->buildplace,
		 "cowdancer-ilistcreate",
		 "/.ilist",
		 "find . -xdev -path ./home -prune -o \\( \\( -type l -o -type f \\) -a -links +1 -print0 \\) | xargs -0 stat --format '%d %i '",
		 NULL))
    {
      /* if there was an error, back off to manual form */

      /* but first, try etch-workaround */
      if (pc->debian_etch_workaround)
	{
	  log_printf(log_warn, "Trying my backup method for working with Debian Etch chroots.");
	  if (forkexeclp("chroot",
			 "chroot",
			 pc->buildplace,
			 "cow-shell",
			 "/bin/true",
			 NULL))
	    {
	      /* I failed, what can I do? noooo */
	      log_printf(log_error, "failed running Debian Etch compatibility backup plan, I'm in panic, eek.");
	      return 1;
	    }
	}
      else
	{
	  /*
	     try the normal backup method

	     It probably doesn't work but try it anyway.
	   */
	  log_printf(log_warn, "cowdancer-ilistcreate failed to run within chroot, falling back to old method");
	  ilistcreate(ilistfile,
		      "find . -xdev -path ./home -prune -o \\( \\( -type l -o -type f \\) -a -links +1 -print0 \\) | xargs -0 stat --format '%d %i '");
	}
    }
  chdir(prevdir);
  free(prevdir);

  log_printf(log_info, "Invoking pbuilder");
  pbuildercommandline[1]="build";
  PBUILDER_ADD_PARAM("--buildplace");
  PBUILDER_ADD_PARAM(pc->buildplace);
  if (pc->buildresult)
    {
      PBUILDER_ADD_PARAM("--buildresult");
      PBUILDER_ADD_PARAM(pc->buildresult);
    }
  if (pc->extrapackages)
    {
      PBUILDER_ADD_PARAM("--extrapackages");
      PBUILDER_ADD_PARAM(pc->extrapackages);
    }
  if (pc->binary_arch)
    {
      PBUILDER_ADD_PARAM("--binary-arch");
    }
  else if (pc->binary_indep)
    {
      PBUILDER_ADD_PARAM("--binary-indep");
    }
  PBUILDER_ADD_PARAM("--no-targz");
  PBUILDER_ADD_PARAM("--internal-chrootexec");
  PBUILDER_ADD_PARAM(buf_chroot);
  PBUILDER_ADD_PARAM(strdup(dscfile));
  PBUILDER_ADD_PARAM(NULL);
  ret=forkexecvp(pbuildercommandline);
  free(buf_chroot);
  if (ret < 128)
    cpbuilder_internal_cleancow(pc);
  else
    log_printf(log_warn, "pbuilder build aborted, not cleaning");

  free(ilistfile);
  return ret;
}

int cpbuilder_create(const struct pbuilderconfig* pc)
{
  int ret;

  mkdir(pc->basepath,0777);
  log_printf(log_info, "Invoking pbuilder");

  pbuildercommandline[1]="create";
  PBUILDER_ADD_PARAM("--buildplace");
  PBUILDER_ADD_PARAM(pc->basepath);
  if (pc->mirror)
    {
      PBUILDER_ADD_PARAM("--mirror");
      PBUILDER_ADD_PARAM(pc->mirror);
    }
  if (pc->arch)
    {
      PBUILDER_ADD_PARAM("--architecture");
      PBUILDER_ADD_PARAM(pc->arch);
    }
  PBUILDER_ADD_PARAM("--distribution");
  PBUILDER_ADD_PARAM(pc->distribution);
  PBUILDER_ADD_PARAM("--no-targz");
  PBUILDER_ADD_PARAM("--extrapackages");
  if (pc->extrapackages)
    {
      char *extrapackages;
      if (0>asprintf(&extrapackages, "%s cowdancer", pc->extrapackages))
	{
	  log_printf(log_error, "cowdancer: out of memory.");
	  ret = 1;
	  goto out;
	}
      PBUILDER_ADD_PARAM(extrapackages);
    }
  else
    {
      PBUILDER_ADD_PARAM("cowdancer");
    }
  PBUILDER_ADD_PARAM(NULL);
  ret=forkexecvp(pbuildercommandline);

out:
  if (ret)
    {
      log_printf(log_error, "pbuilder create failed");
      if (!pc->debug && 0!=rmrf_check_mount(pc->basepath))
	{
	  log_printf(log_error, "Could not remove original tree");
	}
    }
  return ret;
}


/*
  mv basepath basepath-
  mv buildplace basepath
  rm -rf basepath-

  return 0 on success, nonzero on error.
 */
int cpbuilder_internal_saveupdate(const struct pbuilderconfig* pc)
{
  int ret=0;
  char* buf_tmpfile=NULL;

  if (0>asprintf(&buf_tmpfile, "%s-%i-tmp", pc->buildplace, (int)getpid()))
    {
      /* outofmemory */
      return -1;
    }
  log_printf(log_info, "Moving work directory [%s] to final location [%s] and cleaning up old copy",
	 pc->buildplace, pc->basepath);

  if(-1==rename(pc->basepath, buf_tmpfile))
    {
      log_perror("rename");
      ret=-1;
      goto out;
    }
  if(-1==rename(pc->buildplace, pc->basepath))
    {
      log_perror("rename");
      ret=-1;
      goto out;
    }
  if (0!=rmrf_check_mount(buf_tmpfile))
    {
      log_printf(log_error, "Could not remove original tree");
      ret=-1;
      goto out;
    }
 out:
  free(buf_tmpfile);
  return ret;
}


int cpbuilder_login(const struct pbuilderconfig* pc)
{
  char *buf_chroot;
  int ret;

  if (cpbuilder_internal_cowcopy(pc))
    {
      log_printf(log_error, "Failed cowcopy.");
      return 1;
    }

  /* this is the option passed to internal-chrootexec */
  if (0>asprintf(&buf_chroot, "chroot %s cow-shell", pc->buildplace))
    {
      /* outofmemory */
      return -1;
    }
  log_printf(log_info, "Invoking pbuilder");
  pbuildercommandline[1]="login";
  PBUILDER_ADD_PARAM("--buildplace");
  PBUILDER_ADD_PARAM(pc->buildplace);
  PBUILDER_ADD_PARAM("--no-targz");
  PBUILDER_ADD_PARAM("--internal-chrootexec");
  PBUILDER_ADD_PARAM(buf_chroot);
  PBUILDER_ADD_PARAM(NULL);
  ret=forkexecvp(pbuildercommandline);
  free(buf_chroot);
  if (ret < 128)
    {
      if (pc->save_after_login)
	{
	  if (0!=forkexeclp("chroot", "chroot", pc->buildplace, "apt-get", "clean", NULL))
	    ret=-1;
	  if (cpbuilder_internal_saveupdate(pc))
	    ret=-1;
	}
      else
	{
	  cpbuilder_internal_cleancow(pc);
	}
    }
  else
    log_printf(log_warn, "pbuilder login aborted, not cleaning");

  return ret;
}

/*

Mostly a copy of pbuilder login, executes a script.

 */
int cpbuilder_execute(const struct pbuilderconfig* pc, char** av)
{
  char *buf_chroot;
  int ret;
  int i=0;

  if (cpbuilder_internal_cowcopy(pc))
    {
      log_printf(log_error, "Failed cowcopy.");
      return 1;
    }

  if (0>asprintf(&buf_chroot, "chroot %s cow-shell", pc->buildplace))
    {
      /* outofmemory */
      return -1;
    }
  log_printf(log_info, "Invoking pbuilder");
  pbuildercommandline[1]="execute";
  PBUILDER_ADD_PARAM("--buildplace");
  PBUILDER_ADD_PARAM(pc->buildplace);
  PBUILDER_ADD_PARAM("--no-targz");
  PBUILDER_ADD_PARAM("--internal-chrootexec");
  PBUILDER_ADD_PARAM(buf_chroot);

  /* add all additional parameters */
  while(av[i])
    {
      PBUILDER_ADD_PARAM(av[i]);
      i++;
    }
  PBUILDER_ADD_PARAM(NULL);
  ret=forkexecvp(pbuildercommandline);
  free(buf_chroot);
  if (ret < 128)
    {
      if (pc->save_after_login)
	{
	  if (0!=forkexeclp("chroot", "chroot", pc->buildplace, "apt-get", "clean", NULL))
	    ret=-1;
	  if (cpbuilder_internal_saveupdate(pc))
	    ret=-1;
	}
      else
	{
	  cpbuilder_internal_cleancow(pc);
	}
    }
  else
    log_printf(log_warn, "pbuilder execute aborted, not cleaning");

  return ret;
}


/*
   find matching parameter, and check if it's set.
   @returns found=1, not found=0.
 */
int find_matching_param(const char* option)
{
  int i;
  int found=0;
  for (i=0; i<offset; ++i)
    {
      if (!strcmp(pbuildercommandline[i], option))
	{
	  found=1;
	}
    }
  return found;
}

/**
   implement pbuilder update

   @return 0 on success, other values on failure.
 */
int cpbuilder_update(const struct pbuilderconfig* pc)
{
  char * buf_chroot;
  int ret;

  /*
     I want the following actions done:
     NEW=buildplace OLD=basepath

     cp -al OLD NEW
     update in NEW
     mv OLD OLD-
     mv NEW OLD
     rm -rf OLD-

     But if you ignore that part, it's really simple.
  */

  /* This simple process will not disrupt already-running pbuilder-cow session,
     but it will not work with new pbuilder-cow sessions until this process finishes,
     and any failure to pbuilder update will leave crap.
  */

  if (0>asprintf(&buf_chroot,
		 (pc->no_cowdancer_update)?"chroot %s":	/* try to not use cow-shell, when
							   no-cowdancer-update option is used */
		 "chroot %s cow-shell",
		 pc->buildplace))
    {
      /* if outofmemory, die. */
      log_printf(log_error, "Out of memory.");
      return -1;
    }

  if (cpbuilder_internal_cowcopy(pc))
    {
      log_printf(log_error, "Failed cowcopy.");
      return 1;
    }

  log_printf(log_info, "Invoking pbuilder");

  pbuildercommandline[1]="update";
  PBUILDER_ADD_PARAM("--buildplace");
  PBUILDER_ADD_PARAM(pc->buildplace);
  if (find_matching_param("--override-config"))
    {
      if (pc->mirror)
	{
	  PBUILDER_ADD_PARAM("--mirror");
	  PBUILDER_ADD_PARAM(pc->mirror);
	}
      PBUILDER_ADD_PARAM("--distribution");
      PBUILDER_ADD_PARAM(pc->distribution);
    }
  if (pc->extrapackages)
    {
      PBUILDER_ADD_PARAM("--extrapackages");
      PBUILDER_ADD_PARAM(pc->extrapackages);
    }
  PBUILDER_ADD_PARAM("--no-targz");
  PBUILDER_ADD_PARAM("--internal-chrootexec");
  PBUILDER_ADD_PARAM(buf_chroot);
  PBUILDER_ADD_PARAM(NULL);

  ret=forkexecvp(pbuildercommandline);
  if (ret)
    {
      if (ret < 128)
	{
	  log_printf(log_error, "pbuilder update failed");

	  if (!pc->no_cowdancer_update)
	    {
	      /* advise the user to try this option first */
	      log_printf(log_error, "could not update with cowdancer, try --no-cowdancer-update option");
	    }

	  if (!pc->debug && 0!=rmrf_check_mount(pc->buildplace))
	    {
	      log_printf(log_error, "Could not remove original tree");
	    }
	}
      else
	log_printf(log_warn, "pbuilder update aborted, not cleaning");

      /* either way, I don't want to touch the original tree */
      goto out;

    }
  log_printf(log_info, "removing %s working copy", ilist_PRGNAME);
  cpbuilder_internal_saveupdate(pc);
 out:
  free(buf_chroot);
  return ret;
}

int cpbuilder_help(void)
{
  printf("%s [operation] [options]\n"
	 "operation:\n"
	 " --build\n"
	 " --create\n"
	 " --update\n"
	 " --login\n"
	 " --execute\n"
	 " --help\n"
	 " --dumpconfig\n"
	 "options:\n"
	 " --basepath:\n"
	 " --buildplace:\n"
	 " --distribution:\n"
	 " ... and other pbuilder options\n", ilist_PRGNAME
	 );
  return 0;
}

int app_main(int ac, char** av)
{
  setenv("COWDANCER_REUSE","yes",1);
  return parse_parameter(ac, av, "cow");
}
