diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index afe9e58595..20200c7c9b 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -2892,9 +2892,10 @@ def patch_step(self, beginpath=None, patches=None): srcpathsuffix = patch.get('sourcepath', patch.get('copy', '')) # determine whether 'patch' file should be copied rather than applied copy_patch = 'copy' in patch and 'sourcepath' not in patch + options = patch.get('opts', None) # Extra options for patch command - self.log.debug("Source index: %s; patch level: %s; source path suffix: %s; copy patch: %s", - srcind, level, srcpathsuffix, copy_patch) + self.log.debug("Source index: %s; patch level: %s; source path suffix: %s; copy patch: %s; options: %s", + srcind, level, srcpathsuffix, copy_patch, options) if beginpath is None: try: @@ -2910,7 +2911,7 @@ def patch_step(self, beginpath=None, patches=None): src = os.path.abspath(weld_paths(beginpath, srcpathsuffix)) self.log.debug("Applying patch %s in path %s", patch, src) - apply_patch(patch['path'], src, copy=copy_patch, level=level) + apply_patch(patch['path'], src, copy=copy_patch, level=level, options=options) def prepare_step(self, start_dir=True, load_tc_deps_modules=True): """ diff --git a/easybuild/tools/filetools.py b/easybuild/tools/filetools.py index 5a3d4b914e..8e891a0c5a 100644 --- a/easybuild/tools/filetools.py +++ b/easybuild/tools/filetools.py @@ -1554,10 +1554,10 @@ def create_patch_info(patch_spec): Create info dictionary from specified patch spec. """ # Valid keys that can be used in a patch spec dict - valid_keys = ['name', 'copy', 'level', 'sourcepath', 'alt_location'] + valid_keys = ['name', 'copy', 'level', 'sourcepath', 'alt_location', 'opts'] if isinstance(patch_spec, (list, tuple)): - if not len(patch_spec) == 2: + if len(patch_spec) != 2: error_msg = "Unknown patch specification '%s', only 2-element lists/tuples are supported!" raise EasyBuildError(error_msg, str(patch_spec)) @@ -1598,18 +1598,16 @@ def create_patch_info(patch_spec): ) # Dict must contain at least the patchfile name - if 'name' not in patch_info.keys(): + if 'name' not in patch_info: raise EasyBuildError( "Wrong patch spec '%s', when using a dict 'name' entry must be supplied", str(patch_spec), exit_code=EasyBuildExit.EASYCONFIG_ERROR ) - if 'copy' not in patch_info.keys(): + if 'copy' not in patch_info: validate_patch_spec(patch_info['name']) - else: - if 'sourcepath' in patch_info.keys() or 'level' in patch_info.keys(): - raise EasyBuildError("Wrong patch spec '%s', you can't use 'sourcepath' or 'level' with 'copy' (since " - "this implies you want to copy a file to the 'copy' location)", - str(patch_spec)) + elif 'sourcepath' in patch_info or 'level' in patch_info: + raise EasyBuildError(f"Wrong patch spec '{patch_spec}', you can't use 'sourcepath' or 'level' with 'copy' " + "(since this implies you want to copy a file to the 'copy' location)") else: error_msg = ( "Wrong patch spec, should be string, 2-tuple with patch name + argument, or a dict " @@ -1629,10 +1627,11 @@ def validate_patch_spec(patch_spec): ) -def apply_patch(patch_file, dest, fn=None, copy=False, level=None, use_git=False): +def apply_patch(patch_file, dest, fn=None, copy=False, level=None, use_git=False, options=None): """ Apply a patch to source code in directory dest - assume unified diff created with "diff -ru old new" + - options are additional CLI options to pass to patch command Raises EasyBuildError on any error and returns True on success """ @@ -1712,6 +1711,8 @@ def apply_patch(patch_file, dest, fn=None, copy=False, level=None, use_git=False backup_option = '-b ' if build_option('backup_patched_files') else '' patch_cmd = f"patch {backup_option} -p{level} -i {abs_patch_file}" + if options: + patch_cmd += f' {options}' res = run_shell_cmd(patch_cmd, fail_on_error=False, hidden=True, work_dir=abs_dest) if res.exit_code: diff --git a/test/framework/easyblock.py b/test/framework/easyblock.py index 7262a93bcd..ec3eab2200 100644 --- a/test/framework/easyblock.py +++ b/test/framework/easyblock.py @@ -2470,26 +2470,26 @@ def test_exclude_path_to_top_of_module_tree(self): def test_patch_step(self): """Test patch step.""" test_easyconfigs = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'easyconfigs', 'test_ecs') - ec = process_easyconfig(os.path.join(test_easyconfigs, 't', 'toy', 'toy-0.0.eb'))[0] - orig_sources = ec['ec']['sources'][:] + ec = process_easyconfig(os.path.join(test_easyconfigs, 't', 'toy', 'toy-0.0.eb'))[0]['ec'] + orig_sources = ec['sources'][:] toy_patches = [ 'toy-0.0_fix-silly-typo-in-printf-statement.patch', # test for applying patch ('toy-extra.txt', 'toy-0.0'), # test for patch-by-copy ] - self.assertEqual(ec['ec']['patches'], toy_patches) + self.assertEqual(ec['patches'], toy_patches) # test applying patches without sources - ec['ec']['sources'] = [] - eb = EasyBlock(ec['ec']) + ec['sources'] = [] + eb = EasyBlock(ec) with self.mocked_stdout_stderr(): eb.fetch_step() eb.extract_step() self.assertErrorRegex(EasyBuildError, '.*', eb.patch_step) # test actual patching of unpacked sources - ec['ec']['sources'] = orig_sources - eb = EasyBlock(ec['ec']) + ec['sources'] = orig_sources + eb = EasyBlock(ec) with self.mocked_stdout_stderr(): eb.fetch_step() eb.extract_step() @@ -2502,18 +2502,32 @@ def test_patch_step(self): # check again with backup of patched files enabled update_build_option('backup_patched_files', True) - eb = EasyBlock(ec['ec']) + eb = EasyBlock(ec) with self.mocked_stdout_stderr(): eb.fetch_step() eb.extract_step() eb.patch_step() # verify that patches were applied toydir = os.path.join(eb.builddir, 'toy-0.0') + backup_file = os.path.join(toydir, 'toy.source.orig') self.assertEqual(sorted(os.listdir(toydir)), ['toy-extra.txt', 'toy.source', 'toy.source.orig']) self.assertIn("and very proud of it", read_file(os.path.join(toydir, 'toy.source'))) - self.assertNotIn("and very proud of it", read_file(os.path.join(toydir, 'toy.source.orig'))) + self.assertNotIn("and very proud of it", read_file(backup_file)) self.assertEqual(read_file(os.path.join(toydir, 'toy-extra.txt')), 'moar!\n') + # Check with options set + update_build_option('backup_patched_files', False) + ec['patches'] = [{'name': toy_patches[0], 'opts': '--backup --quiet'}] + eb = EasyBlock(ec) + with self.mocked_stdout_stderr(): + eb.fetch_step() + eb.extract_step() + remove_file(backup_file) + eb.patch_step() + # Backup created by manual option + self.assertExists(backup_file) + self.assertRegex(read_file(eb.logfile), fr'patch .*{toy_patches[0]} .*--backup --quiet') + def test_extensions_sanity_check(self): """Test sanity check aspect of extensions.""" init_config(build_options={'silent': True}) diff --git a/test/framework/filetools.py b/test/framework/filetools.py index 525e97f86f..3f30aa88dd 100644 --- a/test/framework/filetools.py +++ b/test/framework/filetools.py @@ -1894,6 +1894,12 @@ def test_apply_patch(self): # trying the patch again should fail self.assertErrorRegex(EasyBuildError, "Couldn't apply patch file", ft.apply_patch, toy_patch, path) + # Passing an option works + with self.mocked_stdout_stderr(): + ft.apply_patch(toy_patch_gz, path, options=' --reverse') + # Change was really removed + self.assertNotIn(pattern, ft.read_file(os.path.join(path, 'toy-0.0', 'toy.source'))) + # test copying of files, both to an existing directory and a non-existing location test_file = os.path.join(self.test_prefix, 'foo.txt') ft.write_file(test_file, '123')