sbang: put sbang in the install_tree (#11598)

`sbang` is not always accessible to users of packages, e.g., if Spack
is installed in someone's home directory and they deploy software
for others.  Avoid this by:

1. Always installing the `sbang` script in the `install_tree`
2. Relocating binaries to point to the copy in the `install_tree` 
   and not the one in the Spack installation.

This PR also:
- ensures that `sbang` is reinstalled if it is modified in Spack
- adds tests
- updates the way `gobject-introspection` patches Makefiles
   to support `sbang`

Co-authored-by: Todd Gamblin <tgamblin@llnl.gov>
This commit is contained in:
Patrick Gartung 2020-10-26 14:37:54 -05:00 committed by GitHub
parent 718150b997
commit 1c2c30a139
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 90 additions and 17 deletions

View file

@ -3,21 +3,30 @@
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import filecmp
import os import os
import stat import stat
import re import re
import sys import sys
import llnl.util.tty as tty import llnl.util.tty as tty
import llnl.util.filesystem as fs
import spack.paths
import spack.modules import spack.modules
import spack.paths
import spack.store
# Character limit for shebang line. Using Linux's 127 characters
# here, as it is the shortest I could find on a modern OS. #: Character limit for shebang line. Using Linux's 127 characters
#: here, as it is the shortest I could find on a modern OS.
shebang_limit = 127 shebang_limit = 127
def sbang_install_path():
"""Location sbang should be installed within Spack's ``install_tree``."""
return os.path.join(spack.store.layout.root, "bin", "sbang")
def shebang_too_long(path): def shebang_too_long(path):
"""Detects whether a file has a shebang line that is too long.""" """Detects whether a file has a shebang line that is too long."""
if not os.path.isfile(path): if not os.path.isfile(path):
@ -42,7 +51,7 @@ def filter_shebang(path):
original = original.decode('UTF-8') original = original.decode('UTF-8')
# This line will be prepended to file # This line will be prepended to file
new_sbang_line = '#!/bin/bash %s/bin/sbang\n' % spack.paths.prefix new_sbang_line = '#!/bin/bash %s\n' % sbang_install_path()
# Skip files that are already using sbang. # Skip files that are already using sbang.
if original.startswith(new_sbang_line): if original.startswith(new_sbang_line):
@ -102,6 +111,26 @@ def filter_shebangs_in_directory(directory, filenames=None):
filter_shebang(path) filter_shebang(path)
def install_sbang():
"""Ensure that ``sbang`` is installed in the root of Spack's install_tree.
This is the shortest known publicly accessible path, and installing
``sbang`` here ensures that users can access the script and that
``sbang`` itself is in a short path.
"""
# copy in a new version of sbang if it differs from what's in spack
sbang_path = sbang_install_path()
if os.path.exists(sbang_path) and filecmp.cmp(
spack.paths.sbang_script, sbang_path):
return
# make $install_tree/bin and copy in a new version of sbang if needed
sbang_bin_dir = os.path.dirname(sbang_path)
fs.mkdirp(sbang_bin_dir)
fs.install(spack.paths.sbang_script, sbang_path)
fs.set_install_permissions(sbang_bin_dir)
def post_install(spec): def post_install(spec):
"""This hook edits scripts so that they call /bin/bash """This hook edits scripts so that they call /bin/bash
$spack_prefix/bin/sbang instead of something longer than the $spack_prefix/bin/sbang instead of something longer than the
@ -111,5 +140,7 @@ def post_install(spec):
tty.debug('SKIP: shebang filtering [external package]') tty.debug('SKIP: shebang filtering [external package]')
return return
install_sbang()
for directory, _, filenames in os.walk(spec.prefix): for directory, _, filenames in os.walk(spec.prefix):
filter_shebangs_in_directory(directory, filenames) filter_shebangs_in_directory(directory, filenames)

View file

@ -25,6 +25,9 @@
#: The spack script itself #: The spack script itself
spack_script = os.path.join(bin_path, "spack") spack_script = os.path.join(bin_path, "spack")
#: The sbang script in the spack installation
sbang_script = os.path.join(bin_path, "sbang")
# spack directory hierarchy # spack directory hierarchy
lib_path = os.path.join(prefix, "lib", "spack") lib_path = os.path.join(prefix, "lib", "spack")
external_path = os.path.join(lib_path, "external") external_path = os.path.join(lib_path, "external")

View file

@ -13,10 +13,11 @@
import shutil import shutil
import filecmp import filecmp
from llnl.util.filesystem import mkdirp import llnl.util.filesystem as fs
import spack.paths import spack.paths
from spack.hooks.sbang import shebang_too_long, filter_shebangs_in_directory import spack.store
import spack.hooks.sbang as sbang
from spack.util.executable import which from spack.util.executable import which
@ -28,7 +29,7 @@
node_line = "#!/this/" + ('x' * 200) + "/is/node\n" node_line = "#!/this/" + ('x' * 200) + "/is/node\n"
node_in_text = ("line\n") * 100 + "lua\n" + ("line\n" * 100) node_in_text = ("line\n") * 100 + "lua\n" + ("line\n" * 100)
node_line_patched = "//!/this/" + ('x' * 200) + "/is/node\n" node_line_patched = "//!/this/" + ('x' * 200) + "/is/node\n"
sbang_line = '#!/bin/bash %s/bin/sbang\n' % spack.paths.prefix sbang_line = '#!/bin/bash %s/bin/sbang\n' % spack.store.layout.root
last_line = "last!\n" last_line = "last!\n"
@ -38,7 +39,7 @@ def __init__(self):
self.tempdir = tempfile.mkdtemp() self.tempdir = tempfile.mkdtemp()
self.directory = os.path.join(self.tempdir, 'dir') self.directory = os.path.join(self.tempdir, 'dir')
mkdirp(self.directory) fs.mkdirp(self.directory)
# Script with short shebang # Script with short shebang
self.short_shebang = os.path.join(self.tempdir, 'short') self.short_shebang = os.path.join(self.tempdir, 'short')
@ -102,15 +103,15 @@ def script_dir():
def test_shebang_handling(script_dir): def test_shebang_handling(script_dir):
assert shebang_too_long(script_dir.lua_shebang) assert sbang.shebang_too_long(script_dir.lua_shebang)
assert shebang_too_long(script_dir.long_shebang) assert sbang.shebang_too_long(script_dir.long_shebang)
assert not shebang_too_long(script_dir.short_shebang) assert not sbang.shebang_too_long(script_dir.short_shebang)
assert not shebang_too_long(script_dir.has_sbang) assert not sbang.shebang_too_long(script_dir.has_sbang)
assert not shebang_too_long(script_dir.binary) assert not sbang.shebang_too_long(script_dir.binary)
assert not shebang_too_long(script_dir.directory) assert not sbang.shebang_too_long(script_dir.directory)
filter_shebangs_in_directory(script_dir.tempdir) sbang.filter_shebangs_in_directory(script_dir.tempdir)
# Make sure this is untouched # Make sure this is untouched
with open(script_dir.short_shebang, 'r') as f: with open(script_dir.short_shebang, 'r') as f:
@ -157,3 +158,41 @@ def test_shebang_handles_non_writable_files(script_dir):
st = os.stat(script_dir.long_shebang) st = os.stat(script_dir.long_shebang)
assert oct(not_writable_mode) == oct(st.st_mode) assert oct(not_writable_mode) == oct(st.st_mode)
def check_sbang():
sbang_path = sbang.sbang_install_path()
sbang_bin_dir = os.path.dirname(sbang_path)
assert sbang_path.startswith(spack.store.layout.root)
assert os.path.exists(sbang_path)
assert fs.is_exe(sbang_path)
status = os.stat(sbang_path)
assert (status.st_mode & 0o777) == 0o755
status = os.stat(sbang_bin_dir)
assert (status.st_mode & 0o777) == 0o755
def test_install_sbang(install_mockery):
sbang_path = sbang.sbang_install_path()
sbang_bin_dir = os.path.dirname(sbang_path)
assert sbang_path.startswith(spack.store.layout.root)
assert not os.path.exists(sbang_bin_dir)
sbang.install_sbang()
check_sbang()
# put an invalid file in for sbang
fs.mkdirp(sbang_bin_dir)
with open(sbang_path, "w") as f:
f.write("foo")
sbang.install_sbang()
check_sbang()
# install again and make sure sbang is still fine
sbang.install_sbang()
check_sbang()

View file

@ -4,7 +4,7 @@
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
from spack import * from spack import *
from spack.paths import spack_root import spack.hooks.sbang as sbang
class GobjectIntrospection(Package): class GobjectIntrospection(Package):
@ -72,7 +72,7 @@ def install(self, spec, prefix):
make("install") make("install")
def setup_build_environment(self, env): def setup_build_environment(self, env):
env.set('SPACK_SBANG', "%s/bin/sbang" % spack_root) env.set('SPACK_SBANG', sbang.sbang_install_path())
@property @property
def parallel(self): def parallel(self):