Repository: conan-io/conan

Issue:
[feature] Conan 1.40 (PR-9361) breaking changes
Sorry about the sloppy title. ;)

We used to have our own method, but then ported to `apply_conandata_patches()`. This worked fine, but with conan 1.40 _(due https://github.com/conan-io/conan/pull/9361)_ all our recipes are broken.

Sure, - this feature is still experimental, so I had to expect something to break during the update. But this PR also removes the possibility to specify a `base_path`. We use this option intensively and it is very useful. We try to take unmodified patches from upstream. But to apply them you often need to set `base_path` (and/or `strip`). 


Discussion:
Hi @sboehmann 

The possibility is encoded now in the ``source`` folder of the new layout. This change had to be done because the feature was broken when using the layouts, due to the duplicated and often incompatible definition.

You should be able to define and make it work with something like:
```python
def layout(self):
        self.folders.source = "mysrc_path"
```

Could you please have a look and see if that works for you?

From what I see in the diff to 1.39, the ``strip`` argument hasn't changed, it is still available in the ``patch()`` function, and it was not available in direct arg in ``apply_conandata_patches()`` in 1.39, the signature of the method hasn't changed here, so the ``strip`` should keep working as it was. If not, please clarify, we could do a patch for next 1.40.1 patch release. Thanks!
Thank you for your quick reply @memsharded.

#### Regarding `patch()`:
The pull request changes two things. 

One is the base directory _(the `root` argument for `patchset.apply`)_. I assume this will work again for me if I implement `layout()` according to your suggestion. This change makes sense and I am perfectly fine with it.

The second change is that the `base_path` argument has been removed. Only this part is a problem for me. I would have to modify a lot of patches now because I can't specify different `base_path` anymore. But I try to use unmodified patches provided by upstream. For these patches I often have to use a different `base_path`. And if I have multiple patches for the same recipe, it may even differ per patch.

My feature request is therefore to add `base_path` back to the `patch` method _(and if set, join `conanfile.source_folder` and `base_path`)_.

#### Regarding `apply_conandata_patches()`:
Sorry, I misremembered. Contrary to what I wrote above, I don't use this method. I wanted to use it, but had to implement it myself to be able to specify the `strip` argument.

So my second feature request, although not quite as important, would be to restore `base_path` here as well, but also to be able to specify the `strip` options in `conandata.yml`. 
Ok, yes, I was a bit confused about the method, if you were talking about the ``patch()`` one, that could make more sense.

Lets ask @lasote for this, he implemented this change, maybe there is another approach, or maybe the ``base_path`` makes sense.
Hi, I understand. I will open a PR to use again the `base_path` as relative to the `conanfile.source_folder`. If a layout is specified, it will use it, otherwise, it will use the base source folder as previously.

About the `strip` argument, I would say if specified in the `conandata.yml`, it will pass it to strip automatically like any other argument.

Resolving patch:
diff --git a/conan/tools/files/patches.py b/conan/tools/files/patches.py
--- a/conan/tools/files/patches.py
+++ b/conan/tools/files/patches.py
@@ -25,9 +25,10 @@ def emit(self, record):
             self._output.info("%s: %s" % (self.patchname, logstr))
 
 
-def patch(conanfile, patch_file=None, patch_string=None, strip=0, fuzz=False, **kwargs):
+def patch(conanfile, base_path=None, patch_file=None, patch_string=None, strip=0, fuzz=False, **kwargs):
     """ Applies a diff from file (patch_file)  or string (patch_string)
         in base_path directory or current dir if None
+    :param base_path: Base path where the patch should be applied.
     :param patch_file: Patch file that should be applied.
     :param patch_string: Patch string that should be applied.
     :param strip: Number of folders to be stripped from the path.
@@ -56,7 +57,8 @@ def patch(conanfile, patch_file=None, patch_string=None, strip=0, fuzz=False, **
     if not patchset:
         raise ConanException("Failed to parse patch: %s" % (patch_file if patch_file else "string"))
 
-    if not patchset.apply(root=conanfile.source_folder, strip=strip, fuzz=fuzz):
+    root = os.path.join(conanfile.source_folder, base_path) if base_path else conanfile.source_folder
+    if not patchset.apply(root, strip=strip, fuzz=fuzz):
         raise ConanException("Failed to apply patch: %s" % patch_file)
 
 
@@ -71,6 +73,7 @@ def apply_conandata_patches(conanfile):
     ```
     patches:
     - patch_file: "patches/0001-buildflatbuffers-cmake.patch"
+      base_path: "source_subfolder"
     - patch_file: "patches/0002-implicit-copy-constructor.patch"
       patch_type: backport
       patch_source: https://github.com/google/flatbuffers/pull/5650


Test patch:
diff --git a/conans/test/functional/tools/test_files.py b/conans/test/functional/tools/test_files.py
--- a/conans/test/functional/tools/test_files.py
+++ b/conans/test/functional/tools/test_files.py
@@ -158,6 +158,36 @@ def build(self):
            ' clang compilers.' in str(client.out)
 
 
+def test_apply_conandata_patches_relative_base_path(mock_patch_ng):
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile
+        from conan.tools.files import apply_conandata_patches
+
+        class Pkg(ConanFile):
+            name = "mypkg"
+            version = "1.11.0"
+
+            def layout(self):
+                self.folders.source = "source_subfolder"
+
+            def build(self):
+                apply_conandata_patches(self)
+        """)
+    conandata_yml = textwrap.dedent("""
+        patches:
+          "1.11.0":
+            - patch_file: "patches/0001-buildflatbuffers-cmake.patch"
+              base_path: "relative_dir"
+    """)
+
+    client = TestClient()
+    client.save({'conanfile.py': conanfile,
+                 'conandata.yml': conandata_yml})
+    client.run('create .')
+
+    assert mock_patch_ng.apply_args[0].endswith(os.path.join('source_subfolder', "relative_dir"))
+    assert mock_patch_ng.apply_args[1:] == (0, False)
+
 
 def test_no_patch_file_entry():
     conanfile = textwrap.dedent("""
diff --git a/conans/test/unittests/tools/files/test_patches.py b/conans/test/unittests/tools/files/test_patches.py
--- a/conans/test/unittests/tools/files/test_patches.py
+++ b/conans/test/unittests/tools/files/test_patches.py
@@ -1,3 +1,5 @@
+import os
+
 import patch_ng
 import pytest
 
@@ -44,6 +46,17 @@ def test_single_patch_file(mock_patch_ng):
     assert len(str(conanfile.output)) == 0
 
 
+def test_base_path(mock_patch_ng):
+    conanfile = ConanFileMock()
+    conanfile.folders.set_base_source("my_source")
+    conanfile.display_name = 'mocked/ref'
+    patch(conanfile, patch_file='patch-file', base_path="subfolder")
+    assert mock_patch_ng.filename == 'patch-file'
+    assert mock_patch_ng.string is None
+    assert mock_patch_ng.apply_args == (os.path.join("my_source", "subfolder"), 0, False)
+    assert len(str(conanfile.output)) == 0
+
+
 def test_single_patch_string(mock_patch_ng):
     conanfile = ConanFileMock()
     conanfile.folders.set_base_source("my_folder")