diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 4b1fa22b81..8c81032820 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -72,12 +72,12 @@ from easybuild.tools.config import install_path, log_path, package_path, source_paths from easybuild.tools.environment import restore_env, sanitize_env from easybuild.tools.filetools import CHECKSUM_TYPE_MD5, CHECKSUM_TYPE_SHA256 -from easybuild.tools.filetools import adjust_permissions, apply_patch, back_up_file, change_dir, convert_name -from easybuild.tools.filetools import compute_checksum, copy_file, derive_alt_pypi_url, diff_files -from easybuild.tools.filetools import download_file, encode_class_name, extract_file, find_backup_name_candidate -from easybuild.tools.filetools import get_source_tarball_from_git, is_alt_pypi_url, is_sha256_checksum, mkdir -from easybuild.tools.filetools import move_file, move_logs, read_file, remove_file, rmtree2, verify_checksum, weld_paths -from easybuild.tools.filetools import write_file +from easybuild.tools.filetools import adjust_permissions, apply_patch, apply_regex_substitutions, back_up_file +from easybuild.tools.filetools import change_dir, convert_name, compute_checksum, copy_file, derive_alt_pypi_url +from easybuild.tools.filetools import diff_files, download_file, encode_class_name, extract_file +from easybuild.tools.filetools import find_backup_name_candidate, get_source_tarball_from_git, is_alt_pypi_url +from easybuild.tools.filetools import is_sha256_checksum, mkdir, move_file, move_logs, read_file, remove_file, rmtree2 +from easybuild.tools.filetools import verify_checksum, weld_paths, write_file from easybuild.tools.hooks import BUILD_STEP, CLEANUP_STEP, CONFIGURE_STEP, EXTENSIONS_STEP, FETCH_STEP, INSTALL_STEP from easybuild.tools.hooks import MODULE_STEP, PACKAGE_STEP, PATCH_STEP, PERMISSIONS_STEP, POSTITER_STEP, POSTPROC_STEP from easybuild.tools.hooks import PREPARE_STEP, READY_STEP, SANITYCHECK_STEP, SOURCE_STEP, TEST_STEP, TESTCASES_STEP @@ -2152,6 +2152,23 @@ def package_step(self): else: self.log.info("Skipping package step (not enabled)") + def fix_shebang(self): + """Fix shebang lines for specified files.""" + for lang in ['perl', 'python']: + fix_shebang_for = self.cfg['fix_%s_shebang_for' % lang] + if fix_shebang_for: + if isinstance(fix_shebang_for, basestring): + fix_shebang_for = [fix_shebang_for] + + shebang = '#!/usr/bin/env %s' % lang + for glob_pattern in fix_shebang_for: + paths = glob.glob(os.path.join(self.installdir, glob_pattern)) + self.log.info("Fixing '%s' shebang to '%s' for files that match '%s': %s", + lang, shebang, glob_pattern, paths) + regex = r'^#!.*/%s[0-9.]*$' % lang + for path in paths: + apply_regex_substitutions(path, [(regex, shebang)], backup=False) + def post_install_step(self): """ Do some postprocessing @@ -2167,6 +2184,8 @@ def post_install_step(self): raise EasyBuildError("Invalid element in 'postinstallcmds', not a string: %s", cmd) run_cmd(cmd, simple=True, log_ok=True, log_all=True) + self.fix_shebang() + def sanity_check_step(self, *args, **kwargs): """ Do a sanity check on the installation diff --git a/easybuild/framework/easyconfig/default.py b/easybuild/framework/easyconfig/default.py index a7fc463aa5..996eb08797 100644 --- a/easybuild/framework/easyconfig/default.py +++ b/easybuild/framework/easyconfig/default.py @@ -91,6 +91,10 @@ 'easyblock': [None, "EasyBlock to use for building; if set to None, an easyblock is selected " "based on the software name", BUILD], 'easybuild_version': [None, "EasyBuild-version this spec-file was written for", BUILD], + 'fix_perl_shebang_for': [None, "List of files for which Perl shebang should be fixed " + "to '#!/usr/bin/env perl' (glob patterns supported)", BUILD], + 'fix_python_shebang_for': [None, "List of files for which Python shebang should be fixed " + "to '#!/usr/bin/env python' (glob patterns supported)", BUILD], 'github_account': ['%(namelower)s', "GitHub account name to be used to resolve template values in source URLs", BUILD], 'hidden': [False, "Install module file as 'hidden' by prefixing its version with '.'", BUILD], diff --git a/test/framework/toy_build.py b/test/framework/toy_build.py index a5fa9be680..ba09b1e90e 100644 --- a/test/framework/toy_build.py +++ b/test/framework/toy_build.py @@ -2199,6 +2199,47 @@ def check_toy_load(depends_on=False): check_toy_load(depends_on=True) + def test_fix_shebang(self): + """Test use of fix_python_shebang_for & co.""" + test_ecs_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs') + toy_ec_txt = read_file(os.path.join(test_ecs_dir, 't', 'toy', 'toy-0.0.eb')) + + test_ec = os.path.join(self.test_prefix, 'test.eb') + + test_ec_txt = '\n'.join([ + toy_ec_txt, + "postinstallcmds = [" + " 'echo \"#!/usr/bin/python\\n# test\" > %(installdir)s/bin/t1.py',", + " 'echo \"#!/software/Python/3.6.6-foss-2018b/bin/python3.6\\n# test\" > %(installdir)s/bin/t2.py',", + " 'echo \"#!/usr/bin/env python\\n# test\" > %(installdir)s/bin/t3.py',", + " 'echo \"#!/usr/bin/perl\\n# test\" > %(installdir)s/bin/t1.pl',", + " 'echo \"#!/software/Perl/5.28.1-GCCcore-7.3.0/bin/perl5\\n# test\" > %(installdir)s/bin/t2.pl',", + " 'echo \"#!/usr/bin/env perl\\n# test\" > %(installdir)s/bin/t3.pl',", + "]", + "fix_python_shebang_for = ['bin/t1.py', 'bin/*.py', 'nosuchdir/*.py']", + "fix_perl_shebang_for = 'bin/*.pl'", + ]) + write_file(test_ec, test_ec_txt) + self.test_toy_build(ec_file=test_ec, raise_error=True) + + toy_bindir = os.path.join(self.test_installpath, 'software', 'toy', '0.0', 'bin') + + # no re.M, this should match at start of file! + py_shebang_regex = re.compile(r'^#!/usr/bin/env python\n# test$') + for pybin in ['t1.py', 't2.py', 't3.py']: + pybin_path = os.path.join(toy_bindir, pybin) + pybin_txt = read_file(pybin_path) + self.assertTrue(py_shebang_regex.match(pybin_txt), + "Pattern '%s' found in %s: %s" % (py_shebang_regex.pattern, pybin_path, pybin_txt)) + + # no re.M, this should match at start of file! + perl_shebang_regex = re.compile(r'^#!/usr/bin/env perl\n# test$') + for perlbin in ['t1.pl', 't2.pl', 't3.pl']: + perlbin_path = os.path.join(toy_bindir, perlbin) + perlbin_txt = read_file(perlbin_path) + self.assertTrue(perl_shebang_regex.match(perlbin_txt), + "Pattern '%s' found in %s: %s" % (perl_shebang_regex.pattern, perlbin_path, perlbin_txt)) + def suite(): """ return all the tests in this file """