Repository: easybuilders/easybuild-framework

Issue:
surprising behavior with patch files that don't have .patch extension
patches hat don't have .patch extension work the same as patches, unless a custom path is provided, eg.

`('patch_without_extension', 'custom/path')`

in that case the patch file is considered a normal file and is just copied to the custom path.

to avoid surprises for users who are unaware of this, it would be better to do one of the following options:
1. never apply the patch if it does not contain the .patch suffix.

2. always apply the patch, unless explicitly specified that it should only be copied, eg. with an extra list/tuple item:
    `('patch_without_extension', 'custom/path', 'copy-only')`.
    this would require updating easyconfigs that do an implicit copy.

3. raise error if it does not contain the .patch suffix and no destination path is given.
    this may break some easyconfigs that are not in the EB repo (although this is unlikely, and it's easily fixed)
    
I think option 3 is the best way to avoid surprises, . note also that the documentation specifically mentions that paches should have .patch extension: https://caylo-easybuild.readthedocs.io/en/latest/Writing_easyconfig_files.html#source-files-and-patches




Discussion:
I think the right way forward here is to print a clear deprecation warning whenever an actual patch file is specified that doesn't have a `.patch` extension, and then hard break that in the upcoming EasyBuild 5.0.
I agree, just need to find time to implement it...
The changes made in #3860 should make this significantly easier, since the refactoring done there resulted in a separate `create_patch_info` function which should be the right place to tackle this issue...

Resolving patch:
diff --git a/easybuild/tools/filetools.py b/easybuild/tools/filetools.py
--- a/easybuild/tools/filetools.py
+++ b/easybuild/tools/filetools.py
@@ -1493,6 +1493,8 @@ def create_patch_info(patch_spec):
                                  str(patch_spec))
 
     elif isinstance(patch_spec, string_type):
+        if not patch_spec.endswith('.patch'):
+            _log.deprecated("Use of patch file with filename that doesn't end with .patch: %s" % patch_spec, '5.0')
         patch_info = {'name': patch_spec}
     else:
         error_msg = "Wrong patch spec, should be string of 2-tuple with patch name + argument: %s"


Test patch:
diff --git a/test/framework/easyconfig.py b/test/framework/easyconfig.py
--- a/test/framework/easyconfig.py
+++ b/test/framework/easyconfig.py
@@ -461,7 +461,7 @@ def test_exts_list(self):
             '   ("ext1", "1.0"),',
             '   ("ext2", "2.0", {',
             '       "source_urls": [("http://example.com", "suffix")],'
-            '       "patches": ["toy-0.0.eb"],',  # dummy patch to avoid downloading fail
+            '       "patches": [("toy-0.0.eb", ".")],',  # dummy patch to avoid downloading fail
             '       "checksums": [',
                         # SHA256 checksum for source (gzip-1.4.eb)
             '           "6a5abcab719cefa95dca4af0db0d2a9d205d68f775a33b452ec0f2b75b6a3a45",',
@@ -488,7 +488,7 @@ def test_exts_list(self):
         self.assertEqual(exts_sources[1]['options'], {
             'checksums': ['6a5abcab719cefa95dca4af0db0d2a9d205d68f775a33b452ec0f2b75b6a3a45',
                           '2d964e0e8f05a7cce0dd83a3e68c9737da14b87b61b8b8b0291d58d4c8d1031c'],
-            'patches': ['toy-0.0.eb'],
+            'patches': [('toy-0.0.eb', '.')],
             'source_tmpl': 'gzip-1.4.eb',
             'source_urls': [('http://example.com', 'suffix')],
         })
diff --git a/test/framework/filetools.py b/test/framework/filetools.py
--- a/test/framework/filetools.py
+++ b/test/framework/filetools.py
@@ -1628,11 +1628,22 @@ def test_create_patch_info(self):
         """Test create_patch_info function."""
 
         self.assertEqual(ft.create_patch_info('foo.patch'), {'name': 'foo.patch'})
-        self.assertEqual(ft.create_patch_info('foo.txt'), {'name': 'foo.txt'})
         self.assertEqual(ft.create_patch_info(('foo.patch', 1)), {'name': 'foo.patch', 'level': 1})
         self.assertEqual(ft.create_patch_info(('foo.patch', 'subdir')), {'name': 'foo.patch', 'sourcepath': 'subdir'})
         self.assertEqual(ft.create_patch_info(('foo.txt', 'subdir')), {'name': 'foo.txt', 'copy': 'subdir'})
 
+        self.allow_deprecated_behaviour()
+        self.mock_stderr(True)
+        self.assertEqual(ft.create_patch_info('foo.txt'), {'name': 'foo.txt'})
+        stderr = self.get_stderr()
+        self.mock_stderr(False)
+        self.disallow_deprecated_behaviour()
+        expected_warning = "Use of patch file with filename that doesn't end with .patch: foo.txt"
+        self.assertTrue(expected_warning in stderr)
+
+        # deprecation warning is treated as an error in context of unit test suite
+        self.assertErrorRegex(EasyBuildError, expected_warning, ft.create_patch_info, 'foo.txt')
+
         # faulty input
         error_msg = "Wrong patch spec"
         self.assertErrorRegex(EasyBuildError, error_msg, ft.create_patch_info, None)