Hello,

it was discussed here 5 years ago (and considered good idea) to add -C option 
to install:
http://lists.gnu.org/archive/html/bug-coreutils/2003-11/msg00017.html

With this option install checks an existing destination file and if it is not 
different (by content, owner, group and mode) from source, the file is not 
installed. Preserving destination's original mtime can significantly decrease 
time of building when a system library is reinstalled but the header files 
are not changed at all.

This option was already introduced in RHEL-4, but it was never accepted by 
upstream. There were some issues which should be solved by this patch.


Kamil
From 9d4ae524b93e3bb2f8cb2c99e22f3f192e8dfae8 Mon Sep 17 00:00:00 2001
From: Kamil Dudka <kdu...@redhat.com>
Date: Fri, 16 Jan 2009 11:58:26 +0100
Subject: [PATCH] install: add -C option to install file only when necessary

* src/install.c (have_same_content): New function to compare files
content.
(need_copy): New function to check if copy is necessary.
(main): Handle new option -C.
(copy_file): Skip file copying if not necessary.
(usage): Show new option -C in --help.
* tests/install/install-C: Basic tests for install -C.
* tests/install/install-C-root: Tests requiring root privileges.
* tests/install/install-C-selinux: Tests requiring SELinux.
* tests/Makefile.am: Add new tests for install -C.
* doc/coreutils.texi: Document new install option -C.
* NEWS: Mention the change.
---
 NEWS                            |    3 +
 doc/coreutils.texi              |    5 ++
 src/install.c                   |  112 ++++++++++++++++++++++++++++++++++++++-
 tests/Makefile.am               |    3 +
 tests/install/install-C         |   75 ++++++++++++++++++++++++++
 tests/install/install-C-root    |   64 ++++++++++++++++++++++
 tests/install/install-C-selinux |   56 +++++++++++++++++++
 7 files changed, 317 insertions(+), 1 deletions(-)
 create mode 100755 tests/install/install-C
 create mode 100755 tests/install/install-C-root
 create mode 100755 tests/install/install-C-selinux

diff --git a/NEWS b/NEWS
index f1b383e..05d6644 100644
--- a/NEWS
+++ b/NEWS
@@ -10,6 +10,9 @@ GNU coreutils NEWS                                    -*- outline -*-
   dd accepts iflag=cio and oflag=cio to open the file in CIO (concurrent I/O)
   mode where this feature is available.
 
+  install -C installs file, unless target already exists and is the same file,
+  in which case the modification time is not changed
+
   ls --color now highlights hard linked files, too
 
   stat -f recognizes the Lustre file system type
diff --git a/doc/coreutils.texi b/doc/coreutils.texi
index d8df107..b1c22e0 100644
--- a/doc/coreutils.texi
+++ b/doc/coreutils.texi
@@ -2123,6 +2123,11 @@ The program accepts the following options.  Also see @ref{Common options}.
 
 @table @samp
 
+...@item -C
+...@opindex -C
+Install file, unless target already exists and is the same file, in which
+case the modification time is not changed.
+
 @item -c
 @itemx --crown-margin
 @opindex -c
diff --git a/src/install.c b/src/install.c
index 9dda05a..03c849a 100644
--- a/src/install.c
+++ b/src/install.c
@@ -31,6 +31,7 @@
 #include "cp-hash.h"
 #include "copy.h"
 #include "filenamecat.h"
+#include "full-read.h"
 #include "mkancesdirs.h"
 #include "mkdir-p.h"
 #include "modechange.h"
@@ -125,6 +126,9 @@ static mode_t dir_mode = DEFAULT_MODE;
    or S_ISGID bits.  */
 static mode_t dir_mode_bits = CHMOD_MODE_BITS;
 
+/* Compare files before installing (-C) */
+static bool copy_only_if_needed;
+
 /* If true, strip executable files after copying them. */
 static bool strip_files;
 
@@ -167,6 +171,90 @@ static struct option const long_options[] =
   {NULL, 0, NULL, 0}
 };
 
+/* Compare content of opened files using file descriptors A_FD and B_FD. Return
+   true if files are equal. */
+static bool
+have_same_content (int a_fd, int b_fd)
+{
+#define CMP_BLOCK_SIZE 65536
+  char a_buff[CMP_BLOCK_SIZE];
+  char b_buff[CMP_BLOCK_SIZE];
+
+  size_t size;
+  while (0 < (size = full_read (a_fd, a_buff, CMP_BLOCK_SIZE))) {
+    if (size != full_read (b_fd, b_buff, CMP_BLOCK_SIZE))
+      return false;
+
+    if (memcmp (a_buff, b_buff, size) != 0)
+      return false;
+  }
+
+  return size == 0;
+#undef CMP_BLOCK_SIZE
+}
+
+/* Return true if copy of file FILE to destination TO is necessary.  */
+static bool
+need_copy (const char *file, const char *to, const struct cp_options *x)
+{
+  struct stat file_s, to_s;
+  int file_fd, to_fd;
+  bool match;
+  security_context_t file_scontext = NULL;
+  security_context_t to_scontext = NULL;
+
+  /* compare files using stat */
+  if (stat (file, &file_s) != 0)
+    return true;
+
+  if (stat (to, &to_s) != 0)
+    return true;
+
+  if (file_s.st_size != to_s.st_size
+      || (to_s.st_mode & CHMOD_MODE_BITS) != mode
+      || (owner_id != (uid_t) -1 && to_s.st_uid != owner_id)
+      || (group_id != (gid_t) -1 && to_s.st_gid != group_id))
+    return true;
+
+  /* compare SELinux context if preserving */
+  if (selinux_enabled && x->preserve_security_context)
+    {
+      if (getfilecon (file, &file_scontext) == -1)
+	return true;
+
+      if (getfilecon (to, &to_scontext) == -1)
+	{
+	  freecon (file_scontext);
+	  return true;
+	}
+
+      match = strcmp (file_scontext, to_scontext) == 0;
+
+      freecon (file_scontext);
+      freecon (to_scontext);
+      if (!match)
+	return true;
+    }
+
+  /* compare files content */
+  file_fd = open (file, O_RDONLY);
+  if (file_fd < 0)
+    return true;
+
+  to_fd = open (to, O_RDONLY);
+  if (to_fd < 0)
+    {
+      close (file_fd);
+      return true;
+    }
+
+  match = have_same_content (file_fd, to_fd);
+
+  close (file_fd);
+  close (to_fd);
+  return !match;
+}
+
 static void
 cp_option_init (struct cp_options *x)
 {
@@ -360,7 +448,7 @@ main (int argc, char **argv)
      we'll actually use backup_suffix_string.  */
   backup_suffix_string = getenv ("SIMPLE_BACKUP_SUFFIX");
 
-  while ((optc = getopt_long (argc, argv, "bcsDdg:m:o:pt:TvS:Z:", long_options,
+  while ((optc = getopt_long (argc, argv, "bcCsDdg:m:o:pt:TvS:Z:", long_options,
 			      NULL)) != -1)
     {
       switch (optc)
@@ -372,6 +460,9 @@ main (int argc, char **argv)
 	  break;
 	case 'c':
 	  break;
+	case 'C':
+	  copy_only_if_needed = true;
+	  break;
 	case 's':
 	  strip_files = true;
 #ifdef SIGCHLD
@@ -528,6 +619,19 @@ main (int argc, char **argv)
     error (0, 0, _("WARNING: ignoring --strip-program option as -s option was "
 		   "not specified"));
 
+  if (copy_only_if_needed && x.preserve_timestamps)
+    {
+      error (0, 0, _("options -C and --preserve-timestamps are mutually "
+		     "exclusive"));
+      usage (EXIT_FAILURE);
+    }
+
+  if (copy_only_if_needed && strip_files)
+    {
+      error (0, 0, _("options -C and --strip are mutually exclusive"));
+      usage (EXIT_FAILURE);
+    }
+
   get_ids ();
 
   if (dir_arg)
@@ -644,6 +748,9 @@ copy_file (const char *from, const char *to, const struct cp_options *x)
 {
   bool copy_into_self;
 
+  if (copy_only_if_needed && !need_copy (from, to, x))
+    return true;
+
   /* Allow installing from non-regular files like /dev/null.
      Charles Karney reported that some Sun version of install allows that
      and that sendmail's installation process relies on the behavior.
@@ -834,6 +941,9 @@ Mandatory arguments to long options are mandatory for short options too.\n\
       --backup[=CONTROL]  make a backup of each existing destination file\n\
   -b                  like --backup but does not accept an argument\n\
   -c                  (ignored)\n\
+  -C                  Install file, unless target already exists and is\n\
+                      the same as the new file, in which case the modification\n\
+                      time won't be changed.\n\
   -d, --directory     treat all arguments as directory names; create all\n\
                         components of the specified directories\n\
 "), stdout);
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 6dce9cd..fe53286 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -24,6 +24,7 @@ root_tests =					\
   cp/cp-a-selinux				\
   cp/preserve-gid				\
   cp/special-bits				\
+  install/install-C-root			\
   ls/capability					\
   ls/nameless-uid				\
   misc/chcon					\
@@ -314,6 +315,8 @@ TESTS =						\
   install/basic-1				\
   install/create-leading			\
   install/d-slashdot				\
+  install/install-C				\
+  install/install-C-selinux			\
   install/strip-program				\
   install/trap					\
   ln/backup-1					\
diff --git a/tests/install/install-C b/tests/install/install-C
new file mode 100755
index 0000000..cd7ebf5
--- /dev/null
+++ b/tests/install/install-C
@@ -0,0 +1,75 @@
+#!/bin/sh
+# Ensure "install -C" works. (basic tests)
+
+# Copyright (C) 2008 Free Software Foundation, Inc.
+
+# 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 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 <http://www.gnu.org/licenses/>.
+
+if test "$VERBOSE" = yes; then
+  set -x
+  ginstall --version
+fi
+
+. $srcdir/test-lib.sh
+
+mode1=0644
+mode2=0755
+
+fail=0
+
+echo test > a || framework_failure
+echo "\`a' -> \`b'" > out_installed_first
+echo "removed \`b'
+\`a' -> \`b'" > out_installed_second
+> out_empty
+
+# destination file does not exist
+ginstall -Cv -m$mode1 a b > out || fail=1
+compare out out_installed_first || fail=1
+
+# destination file exists
+ginstall -Cv -m$mode1 a b > out || fail=1
+compare out out_empty || fail=1
+
+# destination file exists but -C is not given
+ginstall -v -m$mode1 a b > out || fail=1
+compare out out_installed_second || fail=1
+
+# destination file exists but content differs
+echo test1 > a || framework_failure
+ginstall -Cv -m$mode1 a b > out || fail=1
+compare out out_installed_second || fail=1
+ginstall -Cv -m$mode1 a b > out || fail=1
+compare out out_empty || fail=1
+
+# destination file exists but content differs (same size)
+echo test2 > a || framework_failure
+ginstall -Cv -m$mode1 a b > out || fail=1
+compare out out_installed_second || fail=1
+ginstall -Cv -m$mode1 a b > out || fail=1
+compare out out_empty || fail=1
+
+# destination file exists but mode differs
+ginstall -Cv -m$mode2 a b > out || fail=1
+compare out out_installed_second || fail=1
+ginstall -Cv -m$mode2 a b > out || fail=1
+compare out out_empty || fail=1
+
+# options -C and --preserve-timestamps are mutually exclusive
+ginstall -C --preserve-timestamps a b && fail=1
+
+# options -C and --strip are mutually exclusive
+ginstall -C --strip --strip-program=echo a b && fail=1
+
+Exit $fail
diff --git a/tests/install/install-C-root b/tests/install/install-C-root
new file mode 100755
index 0000000..f2c7c2f
--- /dev/null
+++ b/tests/install/install-C-root
@@ -0,0 +1,64 @@
+#!/bin/sh
+# Ensure "install -C" compares owner and group.
+
+# Copyright (C) 2008 Free Software Foundation, Inc.
+
+# 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 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 <http://www.gnu.org/licenses/>.
+
+if test "$VERBOSE" = yes; then
+  set -x
+  ginstall --version
+fi
+
+. $srcdir/test-lib.sh
+require_root_
+
+u1=1
+u2=2
+g1=1
+g2=2
+
+fail=0
+
+echo test > a || framework_failure
+echo "\`a' -> \`b'" > out_installed_first
+echo "removed \`b'
+\`a' -> \`b'" > out_installed_second
+> out_empty
+
+# destination file does not exist
+ginstall -Cv -o$u1 -g$g1 a b > out || fail=1
+compare out out_installed_first || fail=1
+
+# destination file exists
+ginstall -Cv -o$u1 -g$g1 a b > out || fail=1
+compare out out_empty || fail=1
+
+# destination file exists but -C is not given
+ginstall -v -o$u1 -g$g1 a b > out || fail=1
+compare out out_installed_second || fail=1
+
+# destination file exists but owner differs
+ginstall -Cv -o$u2 -g$g1 a b > out || fail=1
+compare out out_installed_second || fail=1
+ginstall -Cv -o$u2 -g$g1 a b > out || fail=1
+compare out out_empty || fail=1
+
+# destination file exists but group differs
+ginstall -Cv -o$u2 -g$g2 a b > out || fail=1
+compare out out_installed_second || fail=1
+ginstall -Cv -o$u2 -g$g2 a b > out || fail=1
+compare out out_empty || fail=1
+
+Exit $fail
diff --git a/tests/install/install-C-selinux b/tests/install/install-C-selinux
new file mode 100755
index 0000000..d1d9540
--- /dev/null
+++ b/tests/install/install-C-selinux
@@ -0,0 +1,56 @@
+#!/bin/sh
+# Ensure "install -C" compares SELinux context.
+
+# Copyright (C) 2008 Free Software Foundation, Inc.
+
+# 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 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 <http://www.gnu.org/licenses/>.
+
+if test "$VERBOSE" = yes; then
+  set -x
+  ginstall --version
+fi
+
+. $srcdir/test-lib.sh
+require_selinux_
+
+fail=0
+
+echo test > a || framework_failure
+chcon -u system_u a || skip_test_ "chcon doesn't work"
+
+echo "\`a' -> \`b'" > out_installed_first
+echo "removed \`b'
+\`a' -> \`b'" > out_installed_second
+> out_empty
+
+# destination file does not exist
+ginstall -Cv --preserve-context a b > out || fail=1
+compare out out_installed_first || fail=1
+
+# destination file exists
+ginstall -Cv --preserve-context a b > out || fail=1
+compare out out_empty || fail=1
+
+# destination file exists but -C is not given
+ginstall -v --preserve-context a b > out || fail=1
+compare out out_installed_second || fail=1
+
+# destination file exists but SELinux context differs
+chcon -u unconfined_u a || skip_test_ "chcon doesn't work"
+ginstall -Cv --preserve-context a b > out || fail=1
+compare out out_installed_second || fail=1
+ginstall -Cv --preserve-context a b > out || fail=1
+compare out out_empty || fail=1
+
+Exit $fail
-- 
1.5.4.3

_______________________________________________
Bug-coreutils mailing list
Bug-coreutils@gnu.org
http://lists.gnu.org/mailman/listinfo/bug-coreutils

Reply via email to