[Buildroot] [PATCH v3 1/4] support/scripts: add fix_rpaths

Samuel Martin s.martin49 at gmail.com
Mon Jul 13 10:17:54 UTC 2015


This pyhton script leverages patchelf program to fix the RPATH of binaries.

It offers 2 actions:
- clear the RPATH;
- set the RPATH using relative paths between every single binary and the
  libraries directories.

Signed-off-by: Samuel Martin <s.martin49 at gmail.com>

---
changes v2->v3:
- no change
---
 support/scripts/fix_rpaths | 302 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 302 insertions(+)
 create mode 100755 support/scripts/fix_rpaths

diff --git a/support/scripts/fix_rpaths b/support/scripts/fix_rpaths
new file mode 100755
index 0000000..1c840e0
--- /dev/null
+++ b/support/scripts/fix_rpaths
@@ -0,0 +1,302 @@
+#!/usr/bin/env python
+##
+## Author(s):
+##  - Samuel Martin <s.martin49 at gmail.com>
+##
+## Copyright (C) 2013 Samuel Martin
+##
+## 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; either version 2 of the License, or
+## (at your option) any later version.
+##
+## 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
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU 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
+##
+""" This script scans a direcotry for EFL files and fix their RPATH, making
+them relative.
+"""
+
+from __future__ import print_function, with_statement
+import os
+import stat
+import subprocess
+import sys
+
+PATCHELF_PROGRAM = "patchelf"
+
+
+# pylint: disable=too-few-public-methods
+class PreservedTime(object):
+    """ With-class ensuring the file's times are preserved
+
+    :param path: File path
+    """
+    # pylint: disable=redefined-outer-name
+    def __init__(self, path):
+        self.path = path
+        self.times = None
+
+    # pylint: disable=invalid-name
+    def __enter__(self):
+        st = os.lstat(self.path)
+        self.times = (st.st_atime, st.st_mtime)
+        return self.path
+
+    # pylint: disable=redefined-builtin
+    def __exit__(self, type, value, traceback):
+        os.utime(self.path, self.times)
+
+
+# pylint: disable=redefined-outer-name
+def is_elf_binary(path):
+    """ Return True if path points to a valid ELF file.
+
+    :param path: File path
+    """
+    if not stat.S_ISREG(os.lstat(path).st_mode):
+        return False
+    with PreservedTime(path):
+        # pylint: disable=invalid-name
+        with open(path, "rb") as fp:
+            data = fp.read(4)
+    return data == b"\x7fELF"
+
+
+def has_rpath(elf_file, patchelf_bin=PATCHELF_PROGRAM):
+    """ Return True if the given ELF file accept a RPATH.
+
+    :param elf_file: ELF file path
+    :param patchelf_bin: patchelf program path
+    """
+    cmd = [patchelf_bin, "--print-rpath", elf_file]
+    with PreservedTime(elf_file):
+        try:
+            subprocess.check_call(cmd, stdout=subprocess.PIPE,
+                                  stderr=subprocess.PIPE)
+            elf_with_rpath = True
+        except subprocess.CalledProcessError as _:
+            elf_with_rpath = False
+    return elf_with_rpath
+
+
+def compute_rpath(binary, libdirs):
+    """ Return the RPATH value (with relative paths to the given libdirs).
+
+    :param binary: ELF binary path
+    :param libdirs: List of library directory paths
+    """
+    bindir = os.path.dirname(binary)
+    relpaths = [os.path.relpath(libdir, bindir) for libdir in libdirs]
+    # reduce the list, but keep its original order
+    # pylint: disable=unnecessary-lambda
+    sorted(set(relpaths), key=lambda x: relpaths.index(x))
+    rpaths = [os.path.join("$ORIGIN", relpath) for relpath in relpaths]
+    rpath = ":".join(rpaths)
+    return rpath
+
+
+def fix_rpath(elf_file, rpath, patchelf_bin=PATCHELF_PROGRAM):
+    """ Fix the ELF file RPATH.
+
+    :param elf_file: ELF file patch
+    :param rpath: New RPATH value
+    :param patchelf_bin: patchelf program path
+    """
+    cmd = [patchelf_bin, "--set-rpath", rpath, elf_file]
+    with PreservedTime(elf_file):
+        subprocess.check_call(cmd, stdout=subprocess.PIPE,
+                              stderr=subprocess.PIPE)
+
+
+def shrink_rpath(elf_file, patchelf_bin=PATCHELF_PROGRAM):
+    """ Shrink the ELF file's RPATH.
+
+    :param elf_file: ELF file patch
+    :param patchelf_bin: patchelf program path
+    """
+    cmd = [patchelf_bin, "--shrink-rpath", elf_file]
+    with PreservedTime(elf_file):
+        subprocess.check_call(cmd, stdout=subprocess.PIPE,
+                              stderr=subprocess.PIPE)
+    print("RPATH cleared: %s" % elf_file)
+
+
+def dump_rpath(elf_file, patchelf_bin=PATCHELF_PROGRAM):
+    """ Return the ELF file's RPATH.
+
+    :param elf_file: ELF file patch
+    :param patchelf_bin: patchelf program path
+    """
+    cmd = [patchelf_bin, "--print-rpath", elf_file]
+    with PreservedTime(elf_file):
+        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+                                stderr=subprocess.PIPE)
+        proc.wait()
+        rpath = proc.communicate()[0].strip()
+    if sys.version_info.major >= 3:
+        def _decode(txt):
+            """ Decode function """
+            return txt.decode()
+    else:
+        def _decode(txt):
+            """ Decode function """
+            return txt
+    return _decode(rpath)
+
+
+def update_rpath(elf_file, libdirs, patchelf_bin=PATCHELF_PROGRAM):
+    """ Return the ELF file's RPATH.
+
+    :param elf_file: ELF file patch
+    :param libdirs: List of library directory paths
+    :param patchelf_bin: patchelf program path
+    """
+    rpath = compute_rpath(elf_file, libdirs)
+    fix_rpath(elf_file, rpath, patchelf_bin=patchelf_bin)
+    print("RPATH set: %s \tRPATH='%s'" % (elf_file,
+                                          dump_rpath(elf_file, patchelf_bin)))
+
+
+def find_files(root, file_filter_func=None, exclude_dirs=None):
+    """ Generator returning files from the root location.
+
+    :param root: Root path to be scan
+    :param file_filter_func: Filter function returning a boolean whether the
+                             file path should be yielded or not
+    :param exclude_dirs: List of directories to be prune from the scan
+    """
+    def dummy_filter(_):
+        """ Dummy filter function. Always return True.
+        """
+        return True
+    if not file_filter_func:
+        file_filter_func = dummy_filter
+    for parent, dirs, files in os.walk(root):
+        for xdir in exclude_dirs:
+            if xdir in dirs:
+                del dirs[dirs.index(xdir)]
+                continue
+            for idx, a_dir in enumerate(dirs):
+                if os.path.join(parent, a_dir).endswith(xdir):
+                    del dirs[idx]
+                    continue
+        for a_file in files:
+            full_path = os.path.join(parent, a_file)
+            if not file_filter_func(full_path):
+                continue
+            yield full_path
+
+
+def scan_and_apply(root, rpath_func, exclude_dirs=None,
+                   patchelf_bin=PATCHELF_PROGRAM):
+    """ Scan and update RPATH on ELF files under the root location.
+
+    The new RPATH value is computed from the binaries's and the libraries
+    directories.
+
+    :param root: Root path to be scan
+    :param libdirs: List of library directory paths
+    :param patchelf_bin: patchelf program path
+    """
+    def file_filter(path):
+        """ Return True if the path points to a valid ELF file accepting RPATH.
+        """
+        # check for valid file (discard non-ELF files and broken symlinks)
+        if not is_elf_binary(path):
+            return False
+        return has_rpath(path)
+    exclude_dirs = exclude_dirs if exclude_dirs else list()
+    for filepath in find_files(root, file_filter_func=file_filter,
+                               exclude_dirs=exclude_dirs):
+        rpath_func(filepath, patchelf_bin=patchelf_bin)
+
+
+def main():
+    """ Main function
+    """
+    import argparse
+    parser = argparse.ArgumentParser(description="""\
+            Update the RPATH in all EFL files in ROOT.
+
+            It can perform 2 types of actions on the EFL files, preserving
+            their times:
+            1) 'set' the RPATH, with relative paths between ELF files and
+              the library directories;
+            2) or 'clear' the RPATH.
+
+            """)
+    parser.add_argument("action", choices=["set", "clear"],
+                        help="""\
+            Action processed on RPATH.
+            'set' updates the RPATH with relative path between each binary and
+                library directories passed via the required --libdirs option.
+            'clear' empties the RPATH of the binaries
+            """)
+    parser.add_argument("rootdir", metavar="ROOT",
+                        help="Root path to scan for RPATH fixup")
+    parser.add_argument("--libdirs", nargs="+", default=list(),
+                        help="""\
+            List of library directory paths (must be sub-location of ROOT)""")
+    parser.add_argument("--exclude-dirs", nargs="+", default=list(),
+                        help="List of directories skipped for RPATH update")
+    parser.add_argument("--patchelf-program", dest="patchelf_bin",
+                        default=PATCHELF_PROGRAM,
+                        help="Path to patchelf program to be used")
+    args = parser.parse_args()
+    # sanitizing arguments
+    action = args.action
+    root = os.path.abspath(args.rootdir)
+    libdirs = [os.path.abspath(l) for l in args.libdirs if os.path.isdir(l)]
+    exclude_dirs = [x for x in args.exclude_dirs]
+    patchelf_bin = os.path.abspath(args.patchelf_bin)
+    # sanity checks
+    if action == "set" and not libdirs:
+        msg = "\nERROR: Setting RPATH requires non-empty --libdirs option\n\n"
+        msg += parser.format_help()
+        raise Exception(msg)
+    if not os.path.exists(root):
+        msg = "\nERROR: ROOT must be an existing path.\n"
+        msg += "\troot: %s\n\n" % root
+        msg += parser.format_help()
+        raise Exception(msg)
+    for libdir in libdirs:
+        if not libdir.startswith(root):
+            msg = "\nERROR: each libdirs must be under the root location.\n"
+            msg += "\troot: %s\n" % root
+            msg += "\tfaulty libdir: %s\n\n" % libdir
+            msg += parser.format_help()
+            raise Exception(msg)
+    if not os.path.exists(patchelf_bin):
+        patchelf_found = False
+        for path in os.environ.get("PATH", "").split(":"):
+            if not path:
+                continue
+            if PATCHELF_PROGRAM in os.listdir(path):
+                patchelf_found = True
+                break
+        if not patchelf_found:
+            msg = "\nERROR: no '%s' program found on the host system.\n\n" % \
+                    PATCHELF_PROGRAM
+            msg += parser.format_help()
+            raise Exception(msg)
+    if args.action == "set":
+        def set_rpath(elf_file, patchelf_bin=PATCHELF_PROGRAM):
+            """ Set RPATH
+            """
+            return update_rpath(elf_file, libdirs, patchelf_bin=patchelf_bin)
+        action = set_rpath
+    elif args.action == "clear":
+        action = shrink_rpath
+    scan_and_apply(root, action, exclude_dirs=exclude_dirs,
+                   patchelf_bin=args.patchelf_bin)
+
+
+if __name__ == "__main__":
+    main()
-- 
2.4.5



More information about the buildroot mailing list