Repository: conan-io/conan

Issue:
conan install causes conanbuildinfo.cmake to be re-generated even if it hasn't changed
I've written my own buildscript that attempts to build my entire project including all dependencies in a single step.

On every build, my script calls `conan install` to install the dependencies and then runs `cmake` and `ninja` to build my own source files. I'm calling `conan install` before every build, because otherwise the build process won't pick up any changes in the consumer `conanfile.py` (for example a version update of a dependency). Unfortunately, every time I call `conan install`, conan will re-generate the `conanbuildinfo.cmake`, even when its contents haven't changed. Now the cmake/ninja step will cause a full rebuild of my own sources, because the timestamp on `conanbuildinfo.cmake` was updated. This rebuild is unnecessary (and takes a long time), since nothing has actually changed. Wouldn't it be better if conan only generated that file if it doesn't yet exist or if any changes were made? Or am I missing something?

I'm running conan 1.3.3 on macOS and my build system is CMake+ninja.



Discussion:
Sounds a reasonable request, and shouldn't be difficult to implement, lets have a look.
Awesome, thanks for your quick response!

For anyone else encountering this issue, as a temporary workaround, I've modified my buildscript to do the following:

- Before calling `conan install`, I check if `conanbuildinfo.cmake` exists. If so, I read its contents into memory and rename the file. Then I call `conan install` and read the newly generated `conanbuildinfo.cmake`. If the contents of the new and the old file matches, I delete the new file and restore the old file. Otherwise I delete the old file.

Here's the code:

```(python)
	conan_buildinfo_path = build_dir + "/conanbuildinfo.cmake"
	old_conan_buildinfo_path = build_dir + "/old_conanbuildinfo.cmake"
	conan_buildinfo_exists = os.path.isfile(conan_buildinfo_path)
	old_conan_buildinfo_data = ""
	if conan_buildinfo_exists:
		with open(conan_buildinfo_path, 'r') as conan_buildinfo_file:
			old_conan_buildinfo_data = conan_buildinfo_file.read()
		os.rename(conan_buildinfo_path, old_conan_buildinfo_path)

        # Call conan install

        if old_conan_buildinfo_data != "":
		with open(conan_buildinfo_path, 'r') as conan_buildinfo_file:
			new_conan_buildinfo_data = conan_buildinfo_file.read()
			if old_conan_buildinfo_data == new_conan_buildinfo_data:
				os.remove(conan_buildinfo_path)
				os.rename(old_conan_buildinfo_path, conan_buildinfo_path)
			else:
				os.remove(old_conan_buildinfo_path)

```

Resolving patch:
diff --git a/conans/client/generators/__init__.py b/conans/client/generators/__init__.py
--- a/conans/client/generators/__init__.py
+++ b/conans/client/generators/__init__.py
@@ -98,11 +98,11 @@ def write_generators(conanfile, path, output):
                 for k, v in content.items():
                     v = normalize(v)
                     output.info("Generator %s created %s" % (generator_name, k))
-                    save(join(path, k), v)
+                    save(join(path, k), v, only_if_modified=True)
             else:
                 content = normalize(content)
                 output.info("Generator %s created %s" % (generator_name, generator.filename))
-                save(join(path, generator.filename), content)
+                save(join(path, generator.filename), content, only_if_modified=True)
         except Exception as e:
             if get_env("CONAN_VERBOSE_TRACEBACK", False):
                 output.error(traceback.format_exc())
diff --git a/conans/util/files.py b/conans/util/files.py
--- a/conans/util/files.py
+++ b/conans/util/files.py
@@ -112,20 +112,28 @@ def save_append(path, content):
         handle.write(to_file_bytes(content))
 
 
-def save(path, content):
+def save(path, content, only_if_modified=False):
     """
     Saves a file with given content
     Params:
         path: path to write file to
-        load: contents to save in the file
+        content: contents to save in the file
+        only_if_modified: file won't be modified if the content hasn't changed
     """
     try:
         os.makedirs(os.path.dirname(path))
     except:
         pass
 
+    new_content = to_file_bytes(content)
+
+    if only_if_modified and os.path.exists(path):
+        old_content = load(path, binary=True)
+        if old_content == new_content:
+            return
+
     with open(path, "wb") as handle:
-        handle.write(to_file_bytes(content))
+        handle.write(new_content)
 
 
 def mkdir_tmp():
@@ -158,9 +166,9 @@ def to_file_bytes(content):
     return content
 
 
-def save_files(path, files):
+def save_files(path, files, only_if_modified=False):
     for name, content in list(files.items()):
-        save(os.path.join(path, name), content)
+        save(os.path.join(path, name), content, only_if_modified=only_if_modified)
 
 
 def load(path, binary=False):


Test patch:
diff --git a/conans/test/util/files_test.py b/conans/test/util/files_test.py
new file mode 100644
--- /dev/null
+++ b/conans/test/util/files_test.py
@@ -0,0 +1,35 @@
+import unittest
+import os
+
+from conans.test.utils.test_files import temp_folder
+from conans.util.files import save
+from time import sleep
+
+
+class SaveTestCase(unittest.TestCase):
+
+    def setUp(self):
+        folder = temp_folder()
+        self.filepath = os.path.join(folder, "file.txt")
+
+        # Save some content and keep timestamp
+        self.content = "my content"
+        save(self.filepath, self.content)
+        self.timestamp = os.path.getmtime(self.filepath)
+        sleep(1)  # precission is seconds, so we need to sleep
+
+    def only_if_modified_true_test(self):
+        save(self.filepath, self.content, only_if_modified=True)
+        self.assertEqual(self.timestamp, os.path.getmtime(self.filepath))
+
+    def only_if_modified_false_test(self):
+        save(self.filepath, self.content, only_if_modified=False)
+        self.assertNotEqual(self.timestamp, os.path.getmtime(self.filepath))
+
+    def modified_only_true_test(self):
+        save(self.filepath, "other content", only_if_modified=True)
+        self.assertNotEqual(self.timestamp, os.path.getmtime(self.filepath))
+
+    def modified_only_false_test(self):
+        save(self.filepath, "other content", only_if_modified=False)
+        self.assertNotEqual(self.timestamp, os.path.getmtime(self.filepath))