Skip to content

Commit d3c0127

Browse files
authored
Merge pull request #4654 from boegel/5.0.x
sync with develop (20240923)
2 parents a2550eb + 58f39ef commit d3c0127

File tree

7 files changed

+146
-5
lines changed

7 files changed

+146
-5
lines changed

RELEASE_NOTES

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ For more detailed information, please see the git log.
44
These release notes can also be consulted at https://easybuild.readthedocs.io/en/latest/Release_notes.html.
55

66

7+
v4.9.4 (22 September 2024)
8+
--------------------------
9+
10+
update/bugfix release
11+
12+
- various enhancements, including:
13+
- set $LMOD_TERSE_DECORATIONS to 'no' to avoid additional info in output produced by 'ml --terse avail' (#4648)
14+
- various bug fixes, including:
15+
- implement workaround for permission error when copying read-only files that have extended attributes set and using Python 3.6 (#4642)
16+
- take into account alternate sysroot for /bin/bash used by run_cmd (#4646)
17+
18+
719
v4.9.3 (14 September 2024)
820
--------------------------
921

easybuild/_deprecated.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def get_output_from_process(proc, read_size=None, asynchronous=False, print_depr
131131
@run_cmd_cache
132132
def run_cmd(cmd, log_ok=True, log_all=False, simple=False, inp=None, regexp=True, log_output=False, path=None,
133133
force_in_dry_run=False, verbose=True, shell=None, trace=True, stream_output=None, asynchronous=False,
134-
with_hooks=True):
134+
with_hooks=True, with_sysroot=True):
135135
"""
136136
Run specified command (in a subshell)
137137
:param cmd: command to run
@@ -149,6 +149,7 @@ def run_cmd(cmd, log_ok=True, log_all=False, simple=False, inp=None, regexp=True
149149
:param stream_output: enable streaming command output to stdout
150150
:param asynchronous: run command asynchronously (returns subprocess.Popen instance if set to True)
151151
:param with_hooks: trigger pre/post run_shell_cmd hooks (if defined)
152+
:param with_sysroot: prepend sysroot to exec_cmd (if defined)
152153
"""
153154

154155
_log.deprecated("run_cmd is deprecated, use run_shell_cmd from easybuild.tools.run instead", '6.0')
@@ -228,6 +229,16 @@ def run_cmd(cmd, log_ok=True, log_all=False, simple=False, inp=None, regexp=True
228229

229230
exec_cmd = "/bin/bash"
230231

232+
# if EasyBuild is configured to use an alternate sysroot,
233+
# we should also run shell commands using the bash shell provided in there,
234+
# since /bin/bash may not be compatible with the alternate sysroot
235+
if with_sysroot:
236+
sysroot = build_option('sysroot')
237+
if sysroot:
238+
sysroot_bin_bash = os.path.join(sysroot, 'bin', 'bash')
239+
if os.path.exists(sysroot_bin_bash):
240+
exec_cmd = sysroot_bin_bash
241+
231242
if not shell:
232243
if isinstance(cmd, list):
233244
exec_cmd = None
@@ -237,6 +248,8 @@ def run_cmd(cmd, log_ok=True, log_all=False, simple=False, inp=None, regexp=True
237248
else:
238249
raise EasyBuildError("Don't know how to prefix with /usr/bin/env for commands of type %s", type(cmd))
239250

251+
_log.info("Using %s as shell for running cmd: %s", exec_cmd, cmd)
252+
240253
if with_hooks:
241254
hooks = load_hooks(build_option('hooks'))
242255
hook_res = run_hook(RUN_SHELL_CMD, hooks, pre_step_hook=True, args=[cmd], kwargs={'work_dir': os.getcwd()})

easybuild/tools/filetools.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,13 @@
4242
"""
4343
import datetime
4444
import difflib
45+
import filecmp
4546
import glob
4647
import hashlib
4748
import inspect
4849
import itertools
4950
import os
51+
import platform
5052
import re
5153
import shutil
5254
import signal
@@ -61,6 +63,7 @@
6163
import urllib.request as std_urllib
6264

6365
from easybuild.base import fancylogger
66+
from easybuild.tools import LooseVersion
6467
# import build_log must stay, to use of EasyBuildLog
6568
from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, CWD_NOTFOUND_ERROR
6669
from easybuild.tools.build_log import dry_run_msg, print_msg, print_warning
@@ -2427,8 +2430,42 @@ def copy_file(path, target_path, force_in_dry_run=False):
24272430
else:
24282431
mkdir(os.path.dirname(target_path), parents=True)
24292432
if path_exists:
2430-
shutil.copy2(path, target_path)
2431-
_log.info("%s copied to %s", path, target_path)
2433+
try:
2434+
# on filesystems that support extended file attributes, copying read-only files with
2435+
# shutil.copy2() will give a PermissionError, when using Python < 3.7
2436+
# see https://bugs.python.org/issue24538
2437+
shutil.copy2(path, target_path)
2438+
_log.info("%s copied to %s", path, target_path)
2439+
# catch the more general OSError instead of PermissionError,
2440+
# since Python 2.7 doesn't support PermissionError
2441+
except OSError as err:
2442+
# if file is writable (not read-only), then we give up since it's not a simple permission error
2443+
if os.path.exists(target_path) and os.stat(target_path).st_mode & stat.S_IWUSR:
2444+
raise EasyBuildError("Failed to copy file %s to %s: %s", path, target_path, err)
2445+
2446+
pyver = LooseVersion(platform.python_version())
2447+
if pyver >= LooseVersion('3.7'):
2448+
raise EasyBuildError("Failed to copy file %s to %s: %s", path, target_path, err)
2449+
elif LooseVersion('3.7') > pyver >= LooseVersion('3'):
2450+
if not isinstance(err, PermissionError):
2451+
raise EasyBuildError("Failed to copy file %s to %s: %s", path, target_path, err)
2452+
2453+
# double-check whether the copy actually succeeded
2454+
if not os.path.exists(target_path) or not filecmp.cmp(path, target_path, shallow=False):
2455+
raise EasyBuildError("Failed to copy file %s to %s: %s", path, target_path, err)
2456+
2457+
try:
2458+
# re-enable user write permissions in target, copy xattrs, then remove write perms again
2459+
adjust_permissions(target_path, stat.S_IWUSR)
2460+
shutil._copyxattr(path, target_path)
2461+
adjust_permissions(target_path, stat.S_IWUSR, add=False)
2462+
except OSError as err:
2463+
raise EasyBuildError("Failed to copy file %s to %s: %s", path, target_path, err)
2464+
2465+
msg = ("Failed to copy extended attributes from file %s to %s, due to a bug in shutil (see "
2466+
"https://bugs.python.org/issue24538). Copy successful with workaround.")
2467+
_log.info(msg, path, target_path)
2468+
24322469
elif os.path.islink(path):
24332470
if os.path.isdir(target_path):
24342471
target_path = os.path.join(target_path, os.path.basename(path))

easybuild/tools/modules.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1477,6 +1477,9 @@ def __init__(self, *args, **kwargs):
14771477
setvar('LMOD_REDIRECT', 'no', verbose=False)
14781478
# disable extended defaults within Lmod (introduced and set as default in Lmod 8.0.7)
14791479
setvar('LMOD_EXTENDED_DEFAULT', 'no', verbose=False)
1480+
# disabled decorations in "ml --terse avail" output
1481+
# (introduced in Lmod 8.8, see also https://github.com/TACC/Lmod/issues/690)
1482+
setvar('LMOD_TERSE_DECORATIONS', 'no', verbose=False)
14801483

14811484
super(Lmod, self).__init__(*args, **kwargs)
14821485
version = LooseVersion(self.version)

easybuild/tools/options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2046,7 +2046,7 @@ def set_tmpdir(tmpdir=None, raise_error=False):
20462046
os.chmod(tmptest_file, 0o700)
20472047
res = run_shell_cmd(tmptest_file, fail_on_error=False, in_dry_run=True, hidden=True, stream_output=False,
20482048
with_hooks=False)
2049-
if res.exit_code:
2049+
if res.exit_code != EasyBuildExit.SUCCESS:
20502050
msg = "The temporary directory (%s) does not allow to execute files. " % tempfile.gettempdir()
20512051
msg += "This can cause problems in the build process, consider using --tmpdir."
20522052
if raise_error:

test/framework/filetools.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
@author: Maxime Boissonneault (Compute Canada, Universite Laval)
3333
"""
3434
import datetime
35+
import filecmp
3536
import glob
3637
import logging
3738
import os
@@ -52,6 +53,8 @@
5253
from easybuild.tools.build_log import EasyBuildError
5354
from easybuild.tools.config import IGNORE, ERROR, WARN, build_option, update_build_option
5455
from easybuild.tools.multidiff import multidiff
56+
from easybuild.tools.run import run_shell_cmd
57+
from easybuild.tools.systemtools import LINUX, get_os_type
5558

5659

5760
class FileToolsTest(EnhancedTestCase):
@@ -1976,6 +1979,49 @@ def test_copy_file(self):
19761979
# However, if we add 'force_in_dry_run=True' it should throw an exception
19771980
self.assertErrorRegex(EasyBuildError, "Could not copy *", ft.copy_file, src, target, force_in_dry_run=True)
19781981

1982+
def test_copy_file_xattr(self):
1983+
"""Test copying a file with extended attributes using copy_file."""
1984+
# test copying a read-only files with extended attributes set
1985+
# first, create a special file with extended attributes
1986+
special_file = os.path.join(self.test_prefix, 'special.txt')
1987+
ft.write_file(special_file, 'special')
1988+
# make read-only, and set extended attributes
1989+
attr = ft.which('attr')
1990+
xattr = ft.which('xattr')
1991+
# try to attr (Linux) or xattr (macOS) to set extended attributes foo=bar
1992+
cmd = None
1993+
if attr:
1994+
cmd = "attr -s foo -V bar %s" % special_file
1995+
elif xattr:
1996+
cmd = "xattr -w foo bar %s" % special_file
1997+
1998+
if cmd:
1999+
with self.mocked_stdout_stderr():
2000+
res = run_shell_cmd(cmd, fail_on_error=False)
2001+
2002+
# need to make file read-only after setting extended attribute
2003+
ft.adjust_permissions(special_file, stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH, add=False)
2004+
2005+
# only proceed if setting extended attribute worked
2006+
if res.exit_code == 0:
2007+
target = os.path.join(self.test_prefix, 'copy.txt')
2008+
ft.copy_file(special_file, target)
2009+
self.assertTrue(os.path.exists(target))
2010+
self.assertTrue(filecmp.cmp(special_file, target, shallow=False))
2011+
2012+
# only verify wheter extended attributes were also copied on Linux,
2013+
# since shutil.copy2 doesn't copy them on macOS;
2014+
# see warning at https://docs.python.org/3/library/shutil.html
2015+
if get_os_type() == LINUX:
2016+
if attr:
2017+
cmd = "attr -g foo %s" % target
2018+
else:
2019+
cmd = "xattr -l %s" % target
2020+
with self.mocked_stdout_stderr():
2021+
res = run_shell_cmd(cmd, fail_on_error=False)
2022+
self.assertEqual(res.exit_code, 0)
2023+
self.assertTrue(res.output.endswith('\nbar\n'))
2024+
19792025
def test_copy_files(self):
19802026
"""Test copy_files function."""
19812027
test_ecs = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs')

test/framework/run.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2087,12 +2087,42 @@ def test_run_shell_cmd_delete_cwd(self):
20872087
f"rm -rf {workdir} && echo 'Working directory removed.'"
20882088
)
20892089

2090-
error_pattern = rf"Failed to return to {workdir} after executing command"
2090+
error_pattern = rf"Failed to return to .*/{os.path.basename(self.test_prefix)}/workdir after executing command"
20912091

20922092
mkdir(workdir, parents=True)
20932093
with self.mocked_stdout_stderr():
20942094
self.assertErrorRegex(EasyBuildError, error_pattern, run_shell_cmd, cmd_workdir_rm, work_dir=workdir)
20952095

2096+
def test_run_cmd_sysroot(self):
2097+
"""Test with_sysroot option of run_cmd function."""
2098+
2099+
# use of run_cmd/run_cmd_qa is deprecated, so we need to allow it here
2100+
self.allow_deprecated_behaviour()
2101+
2102+
# put fake /bin/bash in place that will be picked up when using run_cmd with with_sysroot=True
2103+
bin_bash = os.path.join(self.test_prefix, 'bin', 'bash')
2104+
bin_bash_txt = '\n'.join([
2105+
"#!/bin/bash",
2106+
"echo 'Hi there I am a fake /bin/bash in %s'" % self.test_prefix,
2107+
'/bin/bash "$@"',
2108+
])
2109+
write_file(bin_bash, bin_bash_txt)
2110+
adjust_permissions(bin_bash, stat.S_IXUSR)
2111+
2112+
update_build_option('sysroot', self.test_prefix)
2113+
2114+
with self.mocked_stdout_stderr():
2115+
(out, ec) = run_cmd("echo hello")
2116+
self.assertEqual(ec, 0)
2117+
self.assertTrue(out.startswith("Hi there I am a fake /bin/bash in"))
2118+
self.assertTrue(out.endswith("\nhello\n"))
2119+
2120+
# picking up on alternate sysroot is enabled by default, but can be disabled via with_sysroot=False
2121+
with self.mocked_stdout_stderr():
2122+
(out, ec) = run_cmd("echo hello", with_sysroot=False)
2123+
self.assertEqual(ec, 0)
2124+
self.assertEqual(out, "hello\n")
2125+
20962126

20972127
def suite():
20982128
""" returns all the testcases in this module """

0 commit comments

Comments
 (0)