Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[feature] Add tools (conan.tools) to apply patches (#8650)
* [feature] Add tools (conan.tools) to apply patches * Update conan/tools/files/patches.py Co-authored-by: SSE4 <tomskside@gmail.com> Co-authored-by: SSE4 <tomskside@gmail.com>
- Loading branch information
Showing
5 changed files
with
342 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
from conan.tools.files.files import load, save, mkdir, ftp_download, download, get | ||
from conan.tools.files.patches import patch, apply_conandata_patches |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import logging | ||
|
||
import patch_ng | ||
|
||
from conans.errors import ConanException | ||
|
||
try: | ||
from collections.abc import Iterable | ||
except ImportError: | ||
from collections import Iterable | ||
|
||
|
||
class PatchLogHandler(logging.Handler): | ||
def __init__(self, conanfile, patch_file): | ||
logging.Handler.__init__(self, logging.DEBUG) | ||
self._output = conanfile.output | ||
self.patchname = patch_file or "patch_ng" | ||
|
||
def emit(self, record): | ||
logstr = self.format(record) | ||
if record.levelno == logging.WARN: | ||
self._output.warn("%s: %s" % (self.patchname, logstr)) | ||
else: | ||
self._output.info("%s: %s" % (self.patchname, logstr)) | ||
|
||
|
||
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. | ||
:param output: Stream object. | ||
:param fuzz: Should accept fuzzy patches. | ||
:param kwargs: Extra parameters that can be added and will contribute to output information | ||
""" | ||
|
||
patch_type = kwargs.get('patch_type') | ||
patch_description = kwargs.get('patch_description') | ||
if patch_type or patch_description: | ||
patch_type_str = ' ({})'.format(patch_type) if patch_type else '' | ||
patch_description_str = ': {}'.format(patch_description) if patch_description else '' | ||
conanfile.output.info('Apply patch{}{}'.format(patch_type_str, patch_description_str)) | ||
|
||
patchlog = logging.getLogger("patch_ng") | ||
patchlog.handlers = [] | ||
patchlog.addHandler(PatchLogHandler(conanfile, patch_file)) | ||
|
||
if patch_file: | ||
patchset = patch_ng.fromfile(patch_file) | ||
else: | ||
patchset = patch_ng.fromstring(patch_string.encode()) | ||
|
||
if not patchset: | ||
raise ConanException("Failed to parse patch: %s" % (patch_file if patch_file else "string")) | ||
|
||
if not patchset.apply(root=base_path, strip=strip, fuzz=fuzz): | ||
raise ConanException("Failed to apply patch: %s" % patch_file) | ||
|
||
|
||
def apply_conandata_patches(conanfile): | ||
""" | ||
Applies patches stored in 'conanfile.conan_data' (read from 'conandata.yml' file). It will apply | ||
all the patches under 'patches' entry that matches the given 'conanfile.version'. If versions are | ||
not defined in 'conandata.yml' it will apply all the patches directly under 'patches' keyword. | ||
Example of 'conandata.yml' without versions defined: | ||
``` | ||
patches: | ||
- patch_file: "patches/0001-buildflatbuffers-cmake.patch" | ||
base_path: "source_subfolder" | ||
- patch_file: "patches/0002-implicit-copy-constructor.patch" | ||
base_path: "source_subfolder" | ||
patch_type: backport | ||
patch_source: https://github.com/google/flatbuffers/pull/5650 | ||
patch_description: Needed to build with modern clang compilers (adapted to 1.11.0 tagged sources). | ||
``` | ||
Example of 'conandata.yml' with different patches for different versions: | ||
``` | ||
patches: | ||
"1.11.0": | ||
- patch_file: "patches/0001-buildflatbuffers-cmake.patch" | ||
base_path: "source_subfolder" | ||
- patch_file: "patches/0002-implicit-copy-constructor.patch" | ||
base_path: "source_subfolder" | ||
patch_type: backport | ||
patch_source: https://github.com/google/flatbuffers/pull/5650 | ||
patch_description: Needed to build with modern clang compilers (adapted to 1.11.0 tagged sources). | ||
"1.12.0": | ||
- patch_file: "patches/0001-buildflatbuffers-cmake.patch" | ||
base_path: "source_subfolder" | ||
``` | ||
""" | ||
|
||
patches = conanfile.conan_data.get('patches') | ||
if isinstance(patches, dict): | ||
assert conanfile.version, "Can only be applied if conanfile.version is already defined" | ||
entries = patches.get(conanfile.version, []) | ||
for it in entries: | ||
patch(conanfile, **it) | ||
elif isinstance(patches, Iterable): | ||
for it in patches: | ||
patch(conanfile, **it) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import patch_ng | ||
import pytest | ||
|
||
from conan.tools.files import patch, apply_conandata_patches | ||
from conans.errors import ConanException | ||
from conans.test.utils.mocks import ConanFileMock | ||
|
||
|
||
class MockPatchset: | ||
filename = None | ||
string = None | ||
apply_args = None | ||
|
||
def apply(self, root, strip, fuzz): | ||
self.apply_args = (root, strip, fuzz) | ||
return True | ||
|
||
|
||
@pytest.fixture | ||
def mock_patch_ng(monkeypatch): | ||
mock = MockPatchset() | ||
|
||
def mock_fromfile(filename): | ||
mock.filename = filename | ||
return mock | ||
|
||
def mock_fromstring(string): | ||
mock.string = string | ||
return mock | ||
|
||
monkeypatch.setattr(patch_ng, "fromfile", mock_fromfile) | ||
monkeypatch.setattr(patch_ng, "fromstring", mock_fromstring) | ||
return mock | ||
|
||
|
||
def test_single_patch_file(mock_patch_ng): | ||
conanfile = ConanFileMock() | ||
conanfile.display_name = 'mocked/ref' | ||
patch(conanfile, patch_file='patch-file') | ||
assert mock_patch_ng.filename == 'patch-file' | ||
assert mock_patch_ng.string is None | ||
assert mock_patch_ng.apply_args == (None, 0, False) | ||
assert len(str(conanfile.output)) == 0 | ||
|
||
|
||
def test_single_patch_string(mock_patch_ng): | ||
conanfile = ConanFileMock() | ||
conanfile.display_name = 'mocked/ref' | ||
patch(conanfile, patch_string='patch_string') | ||
assert mock_patch_ng.string == b'patch_string' | ||
assert mock_patch_ng.filename is None | ||
assert mock_patch_ng.apply_args == (None, 0, False) | ||
assert len(str(conanfile.output)) == 0 | ||
|
||
|
||
def test_single_patch_arguments(mock_patch_ng): | ||
conanfile = ConanFileMock() | ||
conanfile.display_name = 'mocked/ref' | ||
patch(conanfile, patch_file='patch-file', base_path='root', strip=23, fuzz=True) | ||
assert mock_patch_ng.filename == 'patch-file' | ||
assert mock_patch_ng.apply_args == ('root', 23, True) | ||
assert len(str(conanfile.output)) == 0 | ||
|
||
|
||
def test_single_patch_type(mock_patch_ng): | ||
conanfile = ConanFileMock() | ||
conanfile.display_name = 'mocked/ref' | ||
patch(conanfile, patch_file='patch-file', patch_type='patch_type') | ||
assert 'Apply patch (patch_type)\n' == str(conanfile.output) | ||
|
||
|
||
def test_single_patch_description(mock_patch_ng): | ||
conanfile = ConanFileMock() | ||
conanfile.display_name = 'mocked/ref' | ||
patch(conanfile, patch_file='patch-file', patch_description='patch_description') | ||
assert 'Apply patch: patch_description\n' == str(conanfile.output) | ||
|
||
|
||
def test_single_patch_extra_fields(mock_patch_ng): | ||
conanfile = ConanFileMock() | ||
conanfile.display_name = 'mocked/ref' | ||
patch(conanfile, patch_file='patch-file', patch_type='patch_type', | ||
patch_description='patch_description') | ||
assert 'Apply patch (patch_type): patch_description\n' == str(conanfile.output) | ||
|
||
|
||
def test_single_no_patchset(monkeypatch): | ||
monkeypatch.setattr(patch_ng, "fromfile", lambda _: None) | ||
|
||
conanfile = ConanFileMock() | ||
conanfile.display_name = 'mocked/ref' | ||
with pytest.raises(ConanException) as excinfo: | ||
patch(conanfile, patch_file='patch-file-failed') | ||
assert 'Failed to parse patch: patch-file-failed' == str(excinfo.value) | ||
|
||
|
||
def test_single_apply_fail(monkeypatch): | ||
class MockedApply: | ||
def apply(self, *args, **kwargs): | ||
return False | ||
|
||
monkeypatch.setattr(patch_ng, "fromfile", lambda _: MockedApply()) | ||
|
||
conanfile = ConanFileMock() | ||
conanfile.display_name = 'mocked/ref' | ||
with pytest.raises(ConanException) as excinfo: | ||
patch(conanfile, patch_file='patch-file-failed') | ||
assert 'Failed to apply patch: patch-file-failed' == str(excinfo.value) | ||
|
||
|
||
def test_multiple_no_version(mock_patch_ng): | ||
conanfile = ConanFileMock() | ||
conanfile.display_name = 'mocked/ref' | ||
conanfile.conan_data = {'patches': [ | ||
{'patch_file': 'patches/0001-buildflatbuffers-cmake.patch', | ||
'base_path': 'source_subfolder', }, | ||
{'patch_file': 'patches/0002-implicit-copy-constructor.patch', | ||
'base_path': 'source_subfolder', | ||
'patch_type': 'backport', | ||
'patch_source': 'https://github.com/google/flatbuffers/pull/5650', | ||
'patch_description': 'Needed to build with modern clang compilers.'} | ||
]} | ||
apply_conandata_patches(conanfile) | ||
assert 'Apply patch (backport): Needed to build with modern clang compilers.\n' \ | ||
== str(conanfile.output) | ||
|
||
|
||
def test_multiple_with_version(mock_patch_ng): | ||
conanfile = ConanFileMock() | ||
conanfile.display_name = 'mocked/ref' | ||
conanfile.conan_data = {'patches': { | ||
"1.11.0": [ | ||
{'patch_file': 'patches/0001-buildflatbuffers-cmake.patch', | ||
'base_path': 'source_subfolder', }, | ||
{'patch_file': 'patches/0002-implicit-copy-constructor.patch', | ||
'base_path': 'source_subfolder', | ||
'patch_type': 'backport', | ||
'patch_source': 'https://github.com/google/flatbuffers/pull/5650', | ||
'patch_description': 'Needed to build with modern clang compilers.'} | ||
], | ||
"1.12.0": [ | ||
{'patch_file': 'patches/0001-buildflatbuffers-cmake.patch', | ||
'base_path': 'source_subfolder', }, | ||
]}} | ||
|
||
with pytest.raises(AssertionError) as excinfo: | ||
apply_conandata_patches(conanfile) | ||
assert 'Can only be applied if conanfile.version is already defined' == str(excinfo.value) | ||
|
||
conanfile.version = "1.2.11" | ||
apply_conandata_patches(conanfile) | ||
assert len(str(conanfile.output)) == 0 | ||
|
||
conanfile.version = "1.11.0" | ||
apply_conandata_patches(conanfile) | ||
assert 'Apply patch (backport): Needed to build with modern clang compilers.\n' \ | ||
== str(conanfile.output) |