Skip to content

Commit bf9ef44

Browse files
committed
Support options for patch command
Allow to pass additional arguments to the `patch` command via the `options` dict key for patch specs
1 parent 0d831da commit bf9ef44

File tree

4 files changed

+45
-22
lines changed

4 files changed

+45
-22
lines changed

easybuild/framework/easyblock.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2892,9 +2892,10 @@ def patch_step(self, beginpath=None, patches=None):
28922892
srcpathsuffix = patch.get('sourcepath', patch.get('copy', ''))
28932893
# determine whether 'patch' file should be copied rather than applied
28942894
copy_patch = 'copy' in patch and 'sourcepath' not in patch
2895+
options = patch.get('opts', None) # Extra options for patch command
28952896

2896-
self.log.debug("Source index: %s; patch level: %s; source path suffix: %s; copy patch: %s",
2897-
srcind, level, srcpathsuffix, copy_patch)
2897+
self.log.debug("Source index: %s; patch level: %s; source path suffix: %s; copy patch: %s; options: %s",
2898+
srcind, level, srcpathsuffix, copy_patch, options)
28982899

28992900
if beginpath is None:
29002901
try:
@@ -2910,7 +2911,7 @@ def patch_step(self, beginpath=None, patches=None):
29102911
src = os.path.abspath(weld_paths(beginpath, srcpathsuffix))
29112912
self.log.debug("Applying patch %s in path %s", patch, src)
29122913

2913-
apply_patch(patch['path'], src, copy=copy_patch, level=level)
2914+
apply_patch(patch['path'], src, copy=copy_patch, level=level, options=options)
29142915

29152916
def prepare_step(self, start_dir=True, load_tc_deps_modules=True):
29162917
"""

easybuild/tools/filetools.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,10 +1554,10 @@ def create_patch_info(patch_spec):
15541554
Create info dictionary from specified patch spec.
15551555
"""
15561556
# Valid keys that can be used in a patch spec dict
1557-
valid_keys = ['name', 'copy', 'level', 'sourcepath', 'alt_location']
1557+
valid_keys = ['name', 'copy', 'level', 'sourcepath', 'alt_location', 'opts']
15581558

15591559
if isinstance(patch_spec, (list, tuple)):
1560-
if not len(patch_spec) == 2:
1560+
if len(patch_spec) != 2:
15611561
error_msg = "Unknown patch specification '%s', only 2-element lists/tuples are supported!"
15621562
raise EasyBuildError(error_msg, str(patch_spec))
15631563

@@ -1598,18 +1598,16 @@ def create_patch_info(patch_spec):
15981598
)
15991599

16001600
# Dict must contain at least the patchfile name
1601-
if 'name' not in patch_info.keys():
1601+
if 'name' not in patch_info:
16021602
raise EasyBuildError(
16031603
"Wrong patch spec '%s', when using a dict 'name' entry must be supplied", str(patch_spec),
16041604
exit_code=EasyBuildExit.EASYCONFIG_ERROR
16051605
)
1606-
if 'copy' not in patch_info.keys():
1606+
if 'copy' not in patch_info:
16071607
validate_patch_spec(patch_info['name'])
1608-
else:
1609-
if 'sourcepath' in patch_info.keys() or 'level' in patch_info.keys():
1610-
raise EasyBuildError("Wrong patch spec '%s', you can't use 'sourcepath' or 'level' with 'copy' (since "
1611-
"this implies you want to copy a file to the 'copy' location)",
1612-
str(patch_spec))
1608+
elif 'sourcepath' in patch_info or 'level' in patch_info:
1609+
raise EasyBuildError(f"Wrong patch spec '{patch_spec}', you can't use 'sourcepath' or 'level' with 'copy' "
1610+
"(since this implies you want to copy a file to the 'copy' location)")
16131611
else:
16141612
error_msg = (
16151613
"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):
16291627
)
16301628

16311629

1632-
def apply_patch(patch_file, dest, fn=None, copy=False, level=None, use_git=False):
1630+
def apply_patch(patch_file, dest, fn=None, copy=False, level=None, use_git=False, options=None):
16331631
"""
16341632
Apply a patch to source code in directory dest
16351633
- assume unified diff created with "diff -ru old new"
1634+
- options are additional CLI options to pass to patch command
16361635
16371636
Raises EasyBuildError on any error and returns True on success
16381637
"""
@@ -1712,6 +1711,8 @@ def apply_patch(patch_file, dest, fn=None, copy=False, level=None, use_git=False
17121711
backup_option = '-b ' if build_option('backup_patched_files') else ''
17131712
patch_cmd = f"patch {backup_option} -p{level} -i {abs_patch_file}"
17141713

1714+
if options:
1715+
patch_cmd += f' {options}'
17151716
res = run_shell_cmd(patch_cmd, fail_on_error=False, hidden=True, work_dir=abs_dest)
17161717

17171718
if res.exit_code:

test/framework/easyblock.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2470,26 +2470,26 @@ def test_exclude_path_to_top_of_module_tree(self):
24702470
def test_patch_step(self):
24712471
"""Test patch step."""
24722472
test_easyconfigs = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'easyconfigs', 'test_ecs')
2473-
ec = process_easyconfig(os.path.join(test_easyconfigs, 't', 'toy', 'toy-0.0.eb'))[0]
2474-
orig_sources = ec['ec']['sources'][:]
2473+
ec = process_easyconfig(os.path.join(test_easyconfigs, 't', 'toy', 'toy-0.0.eb'))[0]['ec']
2474+
orig_sources = ec['sources'][:]
24752475

24762476
toy_patches = [
24772477
'toy-0.0_fix-silly-typo-in-printf-statement.patch', # test for applying patch
24782478
('toy-extra.txt', 'toy-0.0'), # test for patch-by-copy
24792479
]
2480-
self.assertEqual(ec['ec']['patches'], toy_patches)
2480+
self.assertEqual(ec['patches'], toy_patches)
24812481

24822482
# test applying patches without sources
2483-
ec['ec']['sources'] = []
2484-
eb = EasyBlock(ec['ec'])
2483+
ec['sources'] = []
2484+
eb = EasyBlock(ec)
24852485
with self.mocked_stdout_stderr():
24862486
eb.fetch_step()
24872487
eb.extract_step()
24882488
self.assertErrorRegex(EasyBuildError, '.*', eb.patch_step)
24892489

24902490
# test actual patching of unpacked sources
2491-
ec['ec']['sources'] = orig_sources
2492-
eb = EasyBlock(ec['ec'])
2491+
ec['sources'] = orig_sources
2492+
eb = EasyBlock(ec)
24932493
with self.mocked_stdout_stderr():
24942494
eb.fetch_step()
24952495
eb.extract_step()
@@ -2502,18 +2502,32 @@ def test_patch_step(self):
25022502

25032503
# check again with backup of patched files enabled
25042504
update_build_option('backup_patched_files', True)
2505-
eb = EasyBlock(ec['ec'])
2505+
eb = EasyBlock(ec)
25062506
with self.mocked_stdout_stderr():
25072507
eb.fetch_step()
25082508
eb.extract_step()
25092509
eb.patch_step()
25102510
# verify that patches were applied
25112511
toydir = os.path.join(eb.builddir, 'toy-0.0')
2512+
backup_file = os.path.join(toydir, 'toy.source.orig')
25122513
self.assertEqual(sorted(os.listdir(toydir)), ['toy-extra.txt', 'toy.source', 'toy.source.orig'])
25132514
self.assertIn("and very proud of it", read_file(os.path.join(toydir, 'toy.source')))
2514-
self.assertNotIn("and very proud of it", read_file(os.path.join(toydir, 'toy.source.orig')))
2515+
self.assertNotIn("and very proud of it", read_file(backup_file))
25152516
self.assertEqual(read_file(os.path.join(toydir, 'toy-extra.txt')), 'moar!\n')
25162517

2518+
# Check with options set
2519+
update_build_option('backup_patched_files', False)
2520+
ec['patches'] = [{'name': toy_patches[0], 'opts': '--backup --quiet'}]
2521+
eb = EasyBlock(ec)
2522+
with self.mocked_stdout_stderr():
2523+
eb.fetch_step()
2524+
eb.extract_step()
2525+
remove_file(backup_file)
2526+
eb.patch_step()
2527+
# Backup created by manual option
2528+
self.assertExists(backup_file)
2529+
self.assertRegex(read_file(eb.logfile), fr'patch .*{toy_patches[0]} .*--backup --quiet')
2530+
25172531
def test_extensions_sanity_check(self):
25182532
"""Test sanity check aspect of extensions."""
25192533
init_config(build_options={'silent': True})

test/framework/filetools.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1894,6 +1894,13 @@ def test_apply_patch(self):
18941894
# trying the patch again should fail
18951895
self.assertErrorRegex(EasyBuildError, "Couldn't apply patch file", ft.apply_patch, toy_patch, path)
18961896

1897+
# Passing an option works
1898+
with self.mocked_stdout_stderr():
1899+
ft.apply_patch(toy_patch_gz, path, options=' --reverse')
1900+
# Change was really removed
1901+
self.assertNotIn(pattern, ft.read_file(os.path.join(path, 'toy-0.0', 'toy.source')))
1902+
1903+
18971904
# test copying of files, both to an existing directory and a non-existing location
18981905
test_file = os.path.join(self.test_prefix, 'foo.txt')
18991906
ft.write_file(test_file, '123')

0 commit comments

Comments
 (0)