Shorten long shebangs only if the execute permission is set (#26899)

The OS should only interpret shebangs, if a file is executable. 

Thus, there should be no need to modify files where no execute bit is set. 

This solves issues that are e.g. encountered while packaging software as 
COVISE (https://github.com/hlrs-vis/covise), which includes example data
in Tecplot format. The sbang post-install hook is applied to every installed
file that starts with the two characters #!, but this fails on the binary Tecplot
files, as they happen to start with #!TDV. Decoding them with UTF-8 fails 
and an exception is thrown during post_install.

Co-authored-by: Martin Aumüller <aumuell@reserv.at>
This commit is contained in:
Harmen Stoppels 2021-10-22 16:55:19 +02:00 committed by GitHub
parent d274769761
commit 609a42d63b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 54 additions and 0 deletions

View file

@ -126,6 +126,11 @@ def filter_shebangs_in_directory(directory, filenames=None):
if not os.path.isfile(path): if not os.path.isfile(path):
continue continue
# only handle executable files
st = os.stat(path)
if not st.st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH):
continue
# only handle links that resolve within THIS package's prefix. # only handle links that resolve within THIS package's prefix.
if os.path.islink(path): if os.path.islink(path):
real_path = os.path.realpath(path) real_path = os.path.realpath(path)

View file

@ -62,18 +62,27 @@ def __init__(self, sbang_line):
with open(self.short_shebang, 'w') as f: with open(self.short_shebang, 'w') as f:
f.write(short_line) f.write(short_line)
f.write(last_line) f.write(last_line)
self.make_executable(self.short_shebang)
# Script with long shebang # Script with long shebang
self.long_shebang = os.path.join(self.tempdir, 'long') self.long_shebang = os.path.join(self.tempdir, 'long')
with open(self.long_shebang, 'w') as f: with open(self.long_shebang, 'w') as f:
f.write(long_line) f.write(long_line)
f.write(last_line) f.write(last_line)
self.make_executable(self.long_shebang)
# Non-executable script with long shebang
self.nonexec_long_shebang = os.path.join(self.tempdir, 'nonexec_long')
with open(self.nonexec_long_shebang, 'w') as f:
f.write(long_line)
f.write(last_line)
# Lua script with long shebang # Lua script with long shebang
self.lua_shebang = os.path.join(self.tempdir, 'lua') self.lua_shebang = os.path.join(self.tempdir, 'lua')
with open(self.lua_shebang, 'w') as f: with open(self.lua_shebang, 'w') as f:
f.write(lua_line) f.write(lua_line)
f.write(last_line) f.write(last_line)
self.make_executable(self.lua_shebang)
# Lua script with long shebang # Lua script with long shebang
self.lua_textbang = os.path.join(self.tempdir, 'lua_in_text') self.lua_textbang = os.path.join(self.tempdir, 'lua_in_text')
@ -81,12 +90,14 @@ def __init__(self, sbang_line):
f.write(short_line) f.write(short_line)
f.write(lua_in_text) f.write(lua_in_text)
f.write(last_line) f.write(last_line)
self.make_executable(self.lua_textbang)
# Node script with long shebang # Node script with long shebang
self.node_shebang = os.path.join(self.tempdir, 'node') self.node_shebang = os.path.join(self.tempdir, 'node')
with open(self.node_shebang, 'w') as f: with open(self.node_shebang, 'w') as f:
f.write(node_line) f.write(node_line)
f.write(last_line) f.write(last_line)
self.make_executable(self.node_shebang)
# Node script with long shebang # Node script with long shebang
self.node_textbang = os.path.join(self.tempdir, 'node_in_text') self.node_textbang = os.path.join(self.tempdir, 'node_in_text')
@ -94,12 +105,14 @@ def __init__(self, sbang_line):
f.write(short_line) f.write(short_line)
f.write(node_in_text) f.write(node_in_text)
f.write(last_line) f.write(last_line)
self.make_executable(self.node_textbang)
# php script with long shebang # php script with long shebang
self.php_shebang = os.path.join(self.tempdir, 'php') self.php_shebang = os.path.join(self.tempdir, 'php')
with open(self.php_shebang, 'w') as f: with open(self.php_shebang, 'w') as f:
f.write(php_line) f.write(php_line)
f.write(last_line) f.write(last_line)
self.make_executable(self.php_shebang)
# php script with long shebang # php script with long shebang
self.php_textbang = os.path.join(self.tempdir, 'php_in_text') self.php_textbang = os.path.join(self.tempdir, 'php_in_text')
@ -107,6 +120,7 @@ def __init__(self, sbang_line):
f.write(short_line) f.write(short_line)
f.write(php_in_text) f.write(php_in_text)
f.write(last_line) f.write(last_line)
self.make_executable(self.php_textbang)
# Script already using sbang. # Script already using sbang.
self.has_sbang = os.path.join(self.tempdir, 'shebang') self.has_sbang = os.path.join(self.tempdir, 'shebang')
@ -114,15 +128,27 @@ def __init__(self, sbang_line):
f.write(sbang_line) f.write(sbang_line)
f.write(long_line) f.write(long_line)
f.write(last_line) f.write(last_line)
self.make_executable(self.has_sbang)
# Fake binary file. # Fake binary file.
self.binary = os.path.join(self.tempdir, 'binary') self.binary = os.path.join(self.tempdir, 'binary')
tar = which('tar', required=True) tar = which('tar', required=True)
tar('czf', self.binary, self.has_sbang) tar('czf', self.binary, self.has_sbang)
self.make_executable(self.binary)
def destroy(self): def destroy(self):
shutil.rmtree(self.tempdir, ignore_errors=True) shutil.rmtree(self.tempdir, ignore_errors=True)
def make_executable(self, path):
# make a file executable
st = os.stat(path)
executable_mode = st.st_mode \
| stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
os.chmod(path, executable_mode)
st = os.stat(path)
assert oct(executable_mode) == oct(st.st_mode & executable_mode)
@pytest.fixture @pytest.fixture
def script_dir(sbang_line): def script_dir(sbang_line):
@ -134,6 +160,7 @@ def script_dir(sbang_line):
def test_shebang_handling(script_dir, sbang_line): def test_shebang_handling(script_dir, sbang_line):
assert sbang.shebang_too_long(script_dir.lua_shebang) assert sbang.shebang_too_long(script_dir.lua_shebang)
assert sbang.shebang_too_long(script_dir.long_shebang) assert sbang.shebang_too_long(script_dir.long_shebang)
assert sbang.shebang_too_long(script_dir.nonexec_long_shebang)
assert not sbang.shebang_too_long(script_dir.short_shebang) assert not sbang.shebang_too_long(script_dir.short_shebang)
assert not sbang.shebang_too_long(script_dir.has_sbang) assert not sbang.shebang_too_long(script_dir.has_sbang)
@ -153,6 +180,11 @@ def test_shebang_handling(script_dir, sbang_line):
assert f.readline() == long_line assert f.readline() == long_line
assert f.readline() == last_line assert f.readline() == last_line
# Make sure this is untouched
with open(script_dir.nonexec_long_shebang, 'r') as f:
assert f.readline() == long_line
assert f.readline() == last_line
# Make sure this got patched. # Make sure this got patched.
with open(script_dir.lua_shebang, 'r') as f: with open(script_dir.lua_shebang, 'r') as f:
assert f.readline() == sbang_line assert f.readline() == sbang_line
@ -243,3 +275,20 @@ def test_install_sbang_too_long(tmpdir):
assert 'root is too long' in err assert 'root is too long' in err
assert 'exceeds limit' in err assert 'exceeds limit' in err
assert 'cannot patch' in err assert 'cannot patch' in err
def test_sbang_hook_skips_nonexecutable_blobs(tmpdir):
# Write a binary blob to non-executable.sh, with a long interpreter "path"
# consisting of invalid UTF-8. The latter is technically not really necessary for
# the test, but binary blobs accidentally starting with b'#!' usually do not contain
# valid UTF-8, so we also ensure that Spack does not attempt to decode as UTF-8.
contents = b'#!' + b'\x80' * sbang.shebang_limit
file = str(tmpdir.join('non-executable.sh'))
with open(file, 'wb') as f:
f.write(contents)
sbang.filter_shebangs_in_directory(str(tmpdir))
# Make sure there is no sbang shebang.
with open(file, 'rb') as f:
assert b'sbang' not in f.readline()