From fc6aa7374a243928af38fd1e583c6c67146951cd Mon Sep 17 00:00:00 2001
From: Todd Gamblin <tgamblin@llnl.gov>
Date: Sat, 5 Mar 2016 04:18:48 -0800
Subject: [PATCH] Fix #104, #54: issues with overlong shebang in deep
 directories.

This does several things:

- Add `sbang`: a script to run scripts with long shebang lines.
  - Documentation for `sbang` is in `bin/sbang`.

- Add an `sbang` hook that filters the `bin` directory after install
  and modifies any scripts wtih shebangs that are too long to use
  `sbang` instead.

- `sbang` is at the top level, so it should be runnable (not much we
  can do if spack itself is too deep for shebang)
  - `sbang`, when used as the interpreter, runs the *second* shebang
    line it finds in a script.
  - shoud fix issues with too long shebang paths.
---
 bin/sbang                         | 84 +++++++++++++++++++++++++++++++
 lib/spack/llnl/util/filesystem.py | 11 +++-
 lib/spack/spack/hooks/sbang.py    | 74 +++++++++++++++++++++++++++
 3 files changed, 168 insertions(+), 1 deletion(-)
 create mode 100755 bin/sbang
 create mode 100644 lib/spack/spack/hooks/sbang.py

diff --git a/bin/sbang b/bin/sbang
new file mode 100755
index 0000000000..ebfbe2e7a1
--- /dev/null
+++ b/bin/sbang
@@ -0,0 +1,84 @@
+#!/bin/bash
+#
+# `sbang`: Run scripts with long shebang lines.
+#
+# Many operating systems limit the length of shebang lines, making it
+# hard to use interpreters that are deep in the directory hierarchy.
+# `sbang` can run such scripts, either as a shebang interpreter, or
+# directly on the command line.
+#
+# Usage
+# -----------------------------
+# Suppose you have a script, long-shebang.sh, like this:
+#
+#     1    #!/very/long/path/to/some/interpreter
+#     2
+#     3    echo "success!"
+#
+# Invoking this script will result in an error on some OS's.  On
+# Linux, you get this:
+#
+#     $ ./long-shebang.sh
+#     -bash: ./long: /very/long/path/to/some/interp: bad interpreter:
+#            No such file or directory
+#
+# On Mac OS X, the system simply assumes the interpreter is the shell
+# and tries to run with it, which is likely not what you want.
+#
+#
+# `sbang` on the command line
+# -----------------------------
+# You can use `sbang` in two ways.  The first is to use it directly,
+# from the command line, like this:
+#
+#     $ sbang ./long-shebang.sh
+#     success!
+#
+#
+# `sbang` as the interpreter
+# -----------------------------
+# You can also use `sbang` *as* the interpreter for your script. Put
+# `#!/bin/bash /path/to/sbang` on line 1, and move the original
+# shebang to line 2 of the script:
+#
+#     1    #!/bin/bash /path/to/sbang
+#     2    #!/long/path/to/real/interpreter with arguments
+#     3
+#     4    echo "success!"
+#
+#     $ ./long-shebang.sh
+#     success!
+#
+# On Linux, you could shorten line 1 to `#!/path/to/sbang`, but other
+# operating systems like Mac OS X require the interpreter to be a
+# binary, so it's best to use `sbang` as a `bash` argument.
+# Obviously, for this to work, `sbang` needs to have a short enough
+# path that *it* will run without hitting OS limits.
+#
+#
+# How it works
+# -----------------------------
+# `sbang` is a very simple bash script. It looks at the first two
+# lines of a script argument and runs the last line starting with
+# `#!`, with the script as an argument. It also forwards arguments.
+#
+
+# First argument is the script we want to actually run.
+script="$1"
+
+# Search the first two lines of script for interpreters.
+lines=0
+while read line && ((lines < 2)) ; do
+    if [[ "$line" = '#!'* ]]; then
+        interpreter="${line#\#!}"
+    fi
+    lines=$((lines+1))
+done < "$script"
+
+# Invoke any interpreter found, or raise an error if none was found.
+if [ -n "$interpreter" ]; then
+    exec $interpreter "$@"
+else
+    echo "error: sbang found no interpreter in $script"
+    exit 1
+fi
diff --git a/lib/spack/llnl/util/filesystem.py b/lib/spack/llnl/util/filesystem.py
index da3cf96050..10d25bdce8 100644
--- a/lib/spack/llnl/util/filesystem.py
+++ b/lib/spack/llnl/util/filesystem.py
@@ -25,7 +25,8 @@
 __all__ = ['set_install_permissions', 'install', 'install_tree', 'traverse_tree',
            'expand_user', 'working_dir', 'touch', 'touchp', 'mkdirp',
            'force_remove', 'join_path', 'ancestor', 'can_access', 'filter_file',
-           'FileFilter', 'change_sed_delimiter', 'is_exe', 'force_symlink']
+           'FileFilter', 'change_sed_delimiter', 'is_exe', 'force_symlink',
+           'copy_mode', 'unset_executable_mode']
 
 import os
 import sys
@@ -158,6 +159,14 @@ def copy_mode(src, dest):
     os.chmod(dest, dest_mode)
 
 
+def unset_executable_mode(path):
+    mode = os.stat(path).st_mode
+    mode &= ~stat.S_IXUSR
+    mode &= ~stat.S_IXGRP
+    mode &= ~stat.S_IXOTH
+    os.chmod(path, mode)
+
+
 def install(src, dest):
     """Manually install a file to a particular location."""
     tty.debug("Installing %s to %s" % (src, dest))
diff --git a/lib/spack/spack/hooks/sbang.py b/lib/spack/spack/hooks/sbang.py
new file mode 100644
index 0000000000..6117c4809d
--- /dev/null
+++ b/lib/spack/spack/hooks/sbang.py
@@ -0,0 +1,74 @@
+##############################################################################
+# Copyright (c) 2013-2015, Lawrence Livermore National Security, LLC.
+# Produced at the Lawrence Livermore National Laboratory.
+#
+# This file is part of Spack.
+# Written by Todd Gamblin, tgamblin@llnl.gov, All rights reserved.
+# LLNL-CODE-647188
+#
+# For details, see https://github.com/llnl/spack
+# Please also see the LICENSE file for our notice and the LGPL.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License (as published by
+# the Free Software Foundation) version 2.1 dated February 1999.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and
+# conditions of the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##############################################################################
+import os
+
+from llnl.util.filesystem import *
+import llnl.util.tty as tty
+
+import spack
+import spack.modules
+
+# 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
+
+def shebang_too_long(path):
+    """Detects whether an file has a shebang line that is too long."""
+    with open(path, 'r') as script:
+        bytes = script.read(2)
+        if bytes != '#!':
+            return False
+
+        line = bytes + script.readline()
+        return len(line) > shebang_limit
+
+
+def filter_shebang(path):
+    """Adds a second shebang line, using sbang, at the beginning of a file."""
+    backup = path + ".shebang.bak"
+    os.rename(path, backup)
+
+    with open(backup, 'r') as bak_file:
+        original = bak_file.read()
+
+    with open(path, 'w') as new_file:
+        new_file.write('#!/bin/bash %s/bin/sbang\n' % spack.spack_root)
+        new_file.write(original)
+
+    copy_mode(backup, path)
+    unset_executable_mode(backup)
+
+    tty.warn("Patched overly long shebang in %s" % path)
+
+
+def post_install(pkg):
+    """This hook edits scripts so that they call /bin/bash
+       $spack_prefix/bin/sbang instead of something longer than the
+       shebang limit."""
+    for file in os.listdir(pkg.prefix.bin):
+        path = os.path.join(pkg.prefix.bin, file)
+        if shebang_too_long(path):
+            filter_shebang(path)
+
-- 
GitLab