Skip to content

Commit 71cbb00

Browse files
authored
Merge pull request #4650 from easybuilders/4.9.x
release EasyBuild v4.9.4
2 parents f6b28bc + 594136c commit 71cbb00

File tree

9 files changed

+158
-20
lines changed

9 files changed

+158
-20
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/tools/filetools.py

Lines changed: 39 additions & 3 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
@@ -59,7 +61,7 @@
5961
from functools import partial
6062

6163
from easybuild.base import fancylogger
62-
from easybuild.tools import run
64+
from easybuild.tools import LooseVersion, run
6365
# import build_log must stay, to use of EasyBuildLog
6466
from easybuild.tools.build_log import EasyBuildError, dry_run_msg, print_msg, print_warning
6567
from easybuild.tools.config import DEFAULT_WAIT_ON_LOCK_INTERVAL, ERROR, GENERIC_EASYBLOCK_PKG, IGNORE, WARN
@@ -2435,8 +2437,42 @@ def copy_file(path, target_path, force_in_dry_run=False):
24352437
else:
24362438
mkdir(os.path.dirname(target_path), parents=True)
24372439
if path_exists:
2438-
shutil.copy2(path, target_path)
2439-
_log.info("%s copied to %s", path, target_path)
2440+
try:
2441+
# on filesystems that support extended file attributes, copying read-only files with
2442+
# shutil.copy2() will give a PermissionError, when using Python < 3.7
2443+
# see https://bugs.python.org/issue24538
2444+
shutil.copy2(path, target_path)
2445+
_log.info("%s copied to %s", path, target_path)
2446+
# catch the more general OSError instead of PermissionError,
2447+
# since Python 2.7 doesn't support PermissionError
2448+
except OSError as err:
2449+
# if file is writable (not read-only), then we give up since it's not a simple permission error
2450+
if os.path.exists(target_path) and os.stat(target_path).st_mode & stat.S_IWUSR:
2451+
raise EasyBuildError("Failed to copy file %s to %s: %s", path, target_path, err)
2452+
2453+
pyver = LooseVersion(platform.python_version())
2454+
if pyver >= LooseVersion('3.7'):
2455+
raise EasyBuildError("Failed to copy file %s to %s: %s", path, target_path, err)
2456+
elif LooseVersion('3.7') > pyver >= LooseVersion('3'):
2457+
if not isinstance(err, PermissionError):
2458+
raise EasyBuildError("Failed to copy file %s to %s: %s", path, target_path, err)
2459+
2460+
# double-check whether the copy actually succeeded
2461+
if not os.path.exists(target_path) or not filecmp.cmp(path, target_path, shallow=False):
2462+
raise EasyBuildError("Failed to copy file %s to %s: %s", path, target_path, err)
2463+
2464+
try:
2465+
# re-enable user write permissions in target, copy xattrs, then remove write perms again
2466+
adjust_permissions(target_path, stat.S_IWUSR)
2467+
shutil._copyxattr(path, target_path)
2468+
adjust_permissions(target_path, stat.S_IWUSR, add=False)
2469+
except OSError as err:
2470+
raise EasyBuildError("Failed to copy file %s to %s: %s", path, target_path, err)
2471+
2472+
msg = ("Failed to copy extended attributes from file %s to %s, due to a bug in shutil (see "
2473+
"https://bugs.python.org/issue24538). Copy successful with workaround.")
2474+
_log.info(msg, path, target_path)
2475+
24402476
elif os.path.islink(path):
24412477
if os.path.isdir(target_path):
24422478
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
@@ -1448,6 +1448,9 @@ def __init__(self, *args, **kwargs):
14481448
setvar('LMOD_REDIRECT', 'no', verbose=False)
14491449
# disable extended defaults within Lmod (introduced and set as default in Lmod 8.0.7)
14501450
setvar('LMOD_EXTENDED_DEFAULT', 'no', verbose=False)
1451+
# disabled decorations in "ml --terse avail" output
1452+
# (introduced in Lmod 8.8, see also https://github.com/TACC/Lmod/issues/690)
1453+
setvar('LMOD_TERSE_DECORATIONS', 'no', verbose=False)
14511454

14521455
super(Lmod, self).__init__(*args, **kwargs)
14531456
version = StrictVersion(self.version)

easybuild/tools/options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1969,7 +1969,7 @@ def set_tmpdir(tmpdir=None, raise_error=False):
19691969
os.close(fd)
19701970
os.chmod(tmptest_file, 0o700)
19711971
if not run_cmd(tmptest_file, simple=True, log_ok=False, regexp=False, force_in_dry_run=True, trace=False,
1972-
stream_output=False, with_hooks=False):
1972+
stream_output=False, with_hooks=False, with_sysroot=False):
19731973
msg = "The temporary directory (%s) does not allow to execute files. " % tempfile.gettempdir()
19741974
msg += "This can cause problems in the build process, consider using --tmpdir."
19751975
if raise_error:

easybuild/tools/run.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def get_output_from_process(proc, read_size=None, asynchronous=False):
134134
@run_cmd_cache
135135
def run_cmd(cmd, log_ok=True, log_all=False, simple=False, inp=None, regexp=True, log_output=False, path=None,
136136
force_in_dry_run=False, verbose=True, shell=None, trace=True, stream_output=None, asynchronous=False,
137-
with_hooks=True):
137+
with_hooks=True, with_sysroot=True):
138138
"""
139139
Run specified command (in a subshell)
140140
:param cmd: command to run
@@ -152,6 +152,7 @@ def run_cmd(cmd, log_ok=True, log_all=False, simple=False, inp=None, regexp=True
152152
:param stream_output: enable streaming command output to stdout
153153
:param asynchronous: run command asynchronously (returns subprocess.Popen instance if set to True)
154154
:param with_hooks: trigger pre/post run_shell_cmd hooks (if defined)
155+
:param with_sysroot: prepend sysroot to exec_cmd (if defined)
155156
"""
156157
cwd = os.getcwd()
157158

@@ -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/systemtools.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,8 @@ def get_avail_core_count():
275275
core_cnt = int(sum(sched_getaffinity()))
276276
else:
277277
# BSD-type systems
278-
out, _ = run_cmd('sysctl -n hw.ncpu', force_in_dry_run=True, trace=False, stream_output=False, with_hooks=False)
278+
out, _ = run_cmd('sysctl -n hw.ncpu', force_in_dry_run=True, trace=False, stream_output=False,
279+
with_hooks=False, with_sysroot=False)
279280
try:
280281
if int(out) > 0:
281282
core_cnt = int(out)
@@ -312,7 +313,8 @@ def get_total_memory():
312313
elif os_type == DARWIN:
313314
cmd = "sysctl -n hw.memsize"
314315
_log.debug("Trying to determine total memory size on Darwin via cmd '%s'", cmd)
315-
out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, with_hooks=False)
316+
out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, with_hooks=False,
317+
with_sysroot=False)
316318
if ec == 0:
317319
memtotal = int(out.strip()) // (1024**2)
318320

@@ -394,15 +396,16 @@ def get_cpu_vendor():
394396

395397
elif os_type == DARWIN:
396398
cmd = "sysctl -n machdep.cpu.vendor"
397-
out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, log_ok=False, with_hooks=False)
399+
out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, log_ok=False,
400+
with_hooks=False, with_sysroot=False)
398401
out = out.strip()
399402
if ec == 0 and out in VENDOR_IDS:
400403
vendor = VENDOR_IDS[out]
401404
_log.debug("Determined CPU vendor on DARWIN as being '%s' via cmd '%s" % (vendor, cmd))
402405
else:
403406
cmd = "sysctl -n machdep.cpu.brand_string"
404407
out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, log_ok=False,
405-
with_hooks=False)
408+
with_hooks=False, with_sysroot=False)
406409
out = out.strip().split(' ')[0]
407410
if ec == 0 and out in CPU_VENDORS:
408411
vendor = out
@@ -505,7 +508,8 @@ def get_cpu_model():
505508

506509
elif os_type == DARWIN:
507510
cmd = "sysctl -n machdep.cpu.brand_string"
508-
out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, with_hooks=False)
511+
out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, with_hooks=False,
512+
with_sysroot=False)
509513
if ec == 0:
510514
model = out.strip()
511515
_log.debug("Determined CPU model on Darwin using cmd '%s': %s" % (cmd, model))
@@ -550,7 +554,8 @@ def get_cpu_speed():
550554
elif os_type == DARWIN:
551555
cmd = "sysctl -n hw.cpufrequency_max"
552556
_log.debug("Trying to determine CPU frequency on Darwin via cmd '%s'" % cmd)
553-
out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, with_hooks=False)
557+
out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, with_hooks=False,
558+
with_sysroot=False)
554559
out = out.strip()
555560
cpu_freq = None
556561
if ec == 0 and out:
@@ -599,7 +604,7 @@ def get_cpu_features():
599604
cmd = "sysctl -n machdep.cpu.%s" % feature_set
600605
_log.debug("Trying to determine CPU features on Darwin via cmd '%s'", cmd)
601606
out, ec = run_cmd(cmd, force_in_dry_run=True, trace=False, stream_output=False, log_ok=False,
602-
with_hooks=False)
607+
with_hooks=False, with_sysroot=False)
603608
if ec == 0:
604609
cpu_feat.extend(out.strip().lower().split())
605610

@@ -626,8 +631,8 @@ def get_gpu_info():
626631
try:
627632
cmd = "nvidia-smi --query-gpu=gpu_name,driver_version --format=csv,noheader"
628633
_log.debug("Trying to determine NVIDIA GPU info on Linux via cmd '%s'", cmd)
629-
out, ec = run_cmd(cmd, simple=False, log_ok=False, log_all=False,
630-
force_in_dry_run=True, trace=False, stream_output=False, with_hooks=False)
634+
out, ec = run_cmd(cmd, simple=False, log_ok=False, log_all=False, force_in_dry_run=True,
635+
trace=False, stream_output=False, with_hooks=False, with_sysroot=False)
631636
if ec == 0:
632637
for line in out.strip().split('\n'):
633638
nvidia_gpu_info = gpu_info.setdefault('NVIDIA', {})
@@ -645,15 +650,15 @@ def get_gpu_info():
645650
try:
646651
cmd = "rocm-smi --showdriverversion --csv"
647652
_log.debug("Trying to determine AMD GPU driver on Linux via cmd '%s'", cmd)
648-
out, ec = run_cmd(cmd, simple=False, log_ok=False, log_all=False,
649-
force_in_dry_run=True, trace=False, stream_output=False, with_hooks=False)
653+
out, ec = run_cmd(cmd, simple=False, log_ok=False, log_all=False, force_in_dry_run=True,
654+
trace=False, stream_output=False, with_hooks=False, with_sysroot=False)
650655
if ec == 0:
651656
amd_driver = out.strip().split('\n')[1].split(',')[1]
652657

653658
cmd = "rocm-smi --showproductname --csv"
654659
_log.debug("Trying to determine AMD GPU info on Linux via cmd '%s'", cmd)
655-
out, ec = run_cmd(cmd, simple=False, log_ok=False, log_all=False,
656-
force_in_dry_run=True, trace=False, stream_output=False, with_hooks=False)
660+
out, ec = run_cmd(cmd, simple=False, log_ok=False, log_all=False, force_in_dry_run=True,
661+
trace=False, stream_output=False, with_hooks=False, with_sysroot=False)
657662
if ec == 0:
658663
for line in out.strip().split('\n')[1:]:
659664
amd_card_series = line.split(',')[1]
@@ -900,7 +905,7 @@ def get_tool_version(tool, version_option='--version', ignore_ec=False):
900905
Output is returned as a single-line string (newlines are replaced by '; ').
901906
"""
902907
out, ec = run_cmd(' '.join([tool, version_option]), simple=False, log_ok=False, force_in_dry_run=True,
903-
trace=False, stream_output=False, with_hooks=False)
908+
trace=False, stream_output=False, with_hooks=False, with_sysroot=False)
904909
if not ignore_ec and ec:
905910
_log.warning("Failed to determine version of %s using '%s %s': %s" % (tool, tool, version_option, out))
906911
return UNKNOWN

easybuild/tools/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
# recent setuptools versions will *TRANSFORM* something like 'X.Y.Zdev' into 'X.Y.Z.dev0', with a warning like
4646
# UserWarning: Normalizing '2.4.0dev' to '2.4.0.dev0'
4747
# This causes problems further up the dependency chain...
48-
VERSION = LooseVersion('4.9.3')
48+
VERSION = LooseVersion('4.9.4')
4949
UNKNOWN = 'UNKNOWN'
5050
UNKNOWN_EASYBLOCKS_VERSION = '0.0.UNKNOWN.EASYBLOCKS'
5151

test/framework/filetools.py

Lines changed: 44 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
@@ -51,6 +52,8 @@
5152
from easybuild.tools.config import IGNORE, ERROR, build_option, update_build_option
5253
from easybuild.tools.multidiff import multidiff
5354
from easybuild.tools.py2vs3 import StringIO, std_urllib
55+
from easybuild.tools.run import run_cmd
56+
from easybuild.tools.systemtools import LINUX, get_os_type
5457

5558

5659
class FileToolsTest(EnhancedTestCase):
@@ -1912,6 +1915,47 @@ def test_copy_file(self):
19121915
# However, if we add 'force_in_dry_run=True' it should throw an exception
19131916
self.assertErrorRegex(EasyBuildError, "Could not copy *", ft.copy_file, src, target, force_in_dry_run=True)
19141917

1918+
def test_copy_file_xattr(self):
1919+
"""Test copying a file with extended attributes using copy_file."""
1920+
# test copying a read-only files with extended attributes set
1921+
# first, create a special file with extended attributes
1922+
special_file = os.path.join(self.test_prefix, 'special.txt')
1923+
ft.write_file(special_file, 'special')
1924+
# make read-only, and set extended attributes
1925+
attr = ft.which('attr')
1926+
xattr = ft.which('xattr')
1927+
# try to attr (Linux) or xattr (macOS) to set extended attributes foo=bar
1928+
cmd = None
1929+
if attr:
1930+
cmd = "attr -s foo -V bar %s" % special_file
1931+
elif xattr:
1932+
cmd = "xattr -w foo bar %s" % special_file
1933+
1934+
if cmd:
1935+
(_, ec) = run_cmd(cmd, simple=False, log_all=False, log_ok=False)
1936+
1937+
# need to make file read-only after setting extended attribute
1938+
ft.adjust_permissions(special_file, stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH, add=False)
1939+
1940+
# only proceed if setting extended attribute worked
1941+
if ec == 0:
1942+
target = os.path.join(self.test_prefix, 'copy.txt')
1943+
ft.copy_file(special_file, target)
1944+
self.assertTrue(os.path.exists(target))
1945+
self.assertTrue(filecmp.cmp(special_file, target, shallow=False))
1946+
1947+
# only verify wheter extended attributes were also copied on Linux,
1948+
# since shutil.copy2 doesn't copy them on macOS;
1949+
# see warning at https://docs.python.org/3/library/shutil.html
1950+
if get_os_type() == LINUX:
1951+
if attr:
1952+
cmd = "attr -g foo %s" % target
1953+
else:
1954+
cmd = "xattr -l %s" % target
1955+
(out, ec) = run_cmd(cmd, simple=False, log_all=False, log_ok=False)
1956+
self.assertEqual(ec, 0)
1957+
self.assertTrue(out.endswith('\nbar\n'))
1958+
19151959
def test_copy_files(self):
19161960
"""Test copy_files function."""
19171961
test_ecs = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs')

test/framework/run.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,31 @@ def post_run_shell_cmd_hook(cmd, *args, **kwargs):
796796
])
797797
self.assertEqual(stdout, expected_stdout)
798798

799+
def test_run_cmd_sysroot(self):
800+
"""Test with_sysroot option of run_cmd function."""
801+
802+
# put fake /bin/bash in place that will be picked up when using run_cmd with with_sysroot=True
803+
bin_bash = os.path.join(self.test_prefix, 'bin', 'bash')
804+
bin_bash_txt = '\n'.join([
805+
"#!/bin/bash",
806+
"echo 'Hi there I am a fake /bin/bash in %s'" % self.test_prefix,
807+
'/bin/bash "$@"',
808+
])
809+
write_file(bin_bash, bin_bash_txt)
810+
adjust_permissions(bin_bash, stat.S_IXUSR)
811+
812+
update_build_option('sysroot', self.test_prefix)
813+
814+
(out, ec) = run_cmd("echo hello")
815+
self.assertEqual(ec, 0)
816+
self.assertTrue(out.startswith("Hi there I am a fake /bin/bash in"))
817+
self.assertTrue(out.endswith("\nhello\n"))
818+
819+
# picking up on alternate sysroot is enabled by default, but can be disabled via with_sysroot=False
820+
(out, ec) = run_cmd("echo hello", with_sysroot=False)
821+
self.assertEqual(ec, 0)
822+
self.assertEqual(out, "hello\n")
823+
799824

800825
def suite():
801826
""" returns all the testcases in this module """

0 commit comments

Comments
 (0)