text: # Copyright (c) YugaByte, Inc.
  
  import glob
  import logging
  import os
  import shutil
  import stat
  import subprocess
  import sys
  
  from yb.command_util import run_program
  
  
  def add_common_arguments(parser):
      """
      Add command-line arguments common between library_packager_old.py invoked as a script, and
      the yb_release.py script.
      """
      parser.add_argument('--verbose',
                          help='Enable verbose output.',
                          action='store_true')
  
  
  class MacLibraryPackager:
      def __init__(self,
                   build_dir,
                   seed_executable_patterns,
                   dest_dir,
                   verbose_mode=False):
          self.build_dir = os.path.realpath(build_dir)
          if not os.path.exists(self.build_dir):
              raise IOError("Build directory '{}' does not exist".format(self.build_dir))
          self.seed_executable_patterns = seed_executable_patterns
          self.dest_dir = dest_dir
          logging.debug(
              "Traversing the dependency graph of executables/libraries, starting "
              "with seed executable patterns: {}".format(", ".join(seed_executable_patterns)))
          self.nodes_by_digest = {}
          self.nodes_by_path = {}
  
          self.dest_dir = dest_dir
          self.verbose_mode = verbose_mode
  
      def package_binaries(self):
          src = self.build_dir
          dst = self.dest_dir
  
          dst_bin_dir = os.path.join(dst, 'bin')
          dst_lib_dir = os.path.join(dst, 'lib')
  
          try:
              os.makedirs(dst_bin_dir)
          except OSError as e:
              raise RuntimeError('Unable to create directory %s', dst)
  
          logging.debug('Created directory %s', dst)
  
          bin_dir_files = []
          for seed_executable_glob in self.seed_executable_patterns:
              if seed_executable_glob.startswith('bin/'):
                  bin_dir_files.append(os.path.basename(seed_executable_glob))
                  logging.debug("Adding file '%s' to bash_scripts", seed_executable_glob)
              updated_glob = seed_executable_glob.replace('$BUILD_ROOT', self.build_dir)
              if updated_glob != seed_executable_glob:
                  logging.info('Substituting: {} -> {}'.format(seed_executable_glob, updated_glob))
                  seed_executable_glob = updated_glob
              glob_results = glob.glob(seed_executable_glob)
              if not glob_results:
                  raise RuntimeError("No files found matching the pattern '{}'".format(
                      seed_executable_glob))
              for executable in glob_results:
                  shutil.copy(executable, dst_bin_dir)
  
          processed_libs = []
          for bin_file in os.listdir(dst_bin_dir):
              if bin_file.endswith('.sh') or bin_file in bin_dir_files:
                  logging.info("Not modifying rpath for file '%s' because it's not a binary file",
                               bin_file)
                  continue
  
              logging.debug('Processing binary file: %s', bin_file)
              libs = []
  
              os.makedirs(os.path.join(dst, 'lib', bin_file))
              libs = self.fix_load_paths(os.path.join(dst_bin_dir, bin_file),
                                         os.path.join(dst_lib_dir, bin_file),
                                         os.path.join('@loader_path/../lib/', bin_file))
  
              # Elements in libs are absolute paths.
              logging.info('library dependencies for file %s: %s', bin_file, libs)
              for lib in libs:
                  if lib in processed_libs:
                      continue
  
                  # For each library dependency, check whether it already has its own directory (if it
                  # does, a physical copy of this library must exist there). If it doesn't, create it
                  # and copy the physical file there.
                  logging.debug('Processing library: %s', lib)
                  libname = os.path.basename(lib)
                  lib_dir_path = os.path.join(dst, 'lib', libname)
                  if os.path.exists(lib_dir_path):
                      continue
  
                  os.mkdir(lib_dir_path)
                  shutil.copy(lib, lib_dir_path)
  
                  lib_file_path = os.path.join(lib_dir_path, libname)
  
                  new_libs = self.fix_load_paths(lib_file_path, lib_dir_path, '@loader_path')
                  for new_lib in new_libs:
                      if new_lib not in processed_libs and new_lib not in libs:
                          logging.info('Adding dependency %s for library %s', new_lib, lib_file_path)
                          libs.append(new_lib)
                  processed_libs.append(lib)
  
      # Run otool to extract information from an object file. Returns the command's output to stdout,
      # or an empty string if filename is not a valid object file.
      # parameter must include the dash.
      def run_otool(self, parameter, filename):
          result = run_program(['otool', parameter, filename], error_ok=True)
  
          if result.stdout.endswith('is not an object file') or \
                  result.stderr.endswith('The file was not recognized as a valid object file'):
              logging.info("Unable to run 'otool %s %s'. File '%s' is not an object file",
                           filename, parameter, filename)
              return None
  
          if result.returncode != 0:
              raise RuntimeError("Unexpected error running 'otool -l %s': '%s'",
                                 filename, result.stderr)
  
          return result.stdout
  
      def extract_rpaths(self, filename):
          stdout = self.run_otool('-l', filename)
  
          if not stdout:
              return []
  
          rpaths = []
          lines = stdout.splitlines()
          for idx, line in enumerate(lines):
              # Extract rpath. Sample output from 'otool -l':
              # Load command 78
              #          cmd LC_RPATH
              #      cmdsize 72
              #         path /Users/hector/code/yugabyte/thirdparty/installed/common/lib (offset 12)
              if line.strip() == 'cmd LC_RPATH':
                  path_line = lines[idx + 2]
                  if path_line.split()[0] != 'path':
                      raise RuntimeError("Invalid output from 'otool -l %s'. "
                                         "Expecting line to start with 'path'. Got '%s'",
                                         filename, path_line.split()[0])
                  rpaths.append(path_line.split()[1])
  
          return rpaths
  
      def extract_dependency_paths(self, filename, rpaths):
          stdout = self.run_otool('-L', filename)
  
          if not stdout:
              return [], []
  
          dependency_paths = []
          absolute_dependency_paths = []
          lines = stdout.splitlines()
          # Skip the first line that is always the library path.
          for line in lines[1:]:
              path = line.split()[0]
  
              # The paths extracted by using otool -L can be absolute paths or relative paths starting
              # with @rpath or @loader_path. Example:
              # otool -L ./build/debug-clang-dynamic-enterprise/lib/libmaster.dylib
              # ./build/debug-clang-dynamic-enterprise/lib/libmaster.dylib:
              #    @rpath/libmaster.dylib (compatibility version 0.0.0, current version 0.0.0)
              #    @rpath/libtserver.dylib (compatibility version 0.0.0, current version 0.0.0)
              #    @rpath/libtablet.dylib (compatibility version 0.0.0, current version 0.0.0)
              #    /Users/hector/code/yugabyte/thirdparty/installed/uninstrumented/lib/\
              # libsnappy.1.dylib (compatibility version 3.0.0, current version 3.4.0)
              #    /usr/lib/libbz2.1.0.dylib (compatibility version 1.0.0, current version 1.0.5)
              #    /usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.8)
              #
              # So we want to find the absolute paths of those paths that start with @rpath by trying
              # all the rpaths extracted by using 'otool -l'
  
              # If we don't skip system libraries and package it, macOS will complain that the library
              # exists in two different places (in /usr/lib and in our package lib directory).
              if path.startswith('/usr/lib'):
                  continue
              if path.startswith('@rpath'):
                  name = os.path.basename(path)
                  # Find the absolute path by prepending all the rpaths extracted from the file.
                  for rpath in rpaths:
                      candidate_path = os.path.join(rpath, name)
                      if os.path.isfile(candidate_path):
                          absolute_dependency_paths.append(candidate_path)
                          break
              elif not path.startswith('@loader_path'):
                  # This should be an absolute path.
                  if os.path.isfile(path):
                      absolute_dependency_paths.append(path)
                  else:
                      raise RuntimeError("File %s doesn't exist", path)
  
              dependency_paths.append(path)
  
          return dependency_paths, absolute_dependency_paths
  
      def remove_rpaths(self, filename, rpaths):
          for rpath in rpaths:
              run_program(['install_name_tool', '-delete_rpath', rpath, filename])
              logging.info('Successfully removed rpath %s from %s', rpath, filename)
  
      def set_new_path(self, filename, old_path, new_path):
          # We need to use a different command if the path is pointing to itself. Example:
          # otool - L ./build/debug-clang-dynamic-enterprise/lib/libmaster.dylib
          # ./build/debug-clang-dynamic-enterprise/ lib/libmaster.dylib:
          #      @rpath/libmaster.dylib
  
          cmd = []
          if os.path.basename(filename) == os.path.basename(old_path):
              run_program(['install_name_tool', '-id', new_path, filename])
              logging.debug('install_name_tool -id %s %s', new_path, filename)
          else:
              run_program(['install_name_tool', '-change', old_path, new_path, filename])
              logging.debug('install_name_tool -change %s %s %s', old_path, new_path, filename)
  
      def fix_load_paths(self, filename, lib_bin_dir, loader_path):
          logging.debug('Processing file %s', filename)
  
          original_mode = os.stat(filename).st_mode
          # Made the file writable.
          try:
              os.chmod(filename, os.stat(filename).st_mode | stat.S_IWUSR)
          except OSError as e:
              logging.error('Unable to make file %s writable', filename)
              raise
  
          # Extract the paths that are used to resolve paths that start with @rpath.
          rpaths = self.extract_rpaths(filename)
  
          # Remove rpaths since we are only going to use @loader_path and absolute paths.
          self.remove_rpaths(filename, rpaths)
  
          # Dependency path will have the paths as extracted by 'otool -L'.
          dependency_paths, absolute_dependency_paths = self.extract_dependency_paths(filename,
                                                                                      rpaths)
  
          logging.debug('Absolute_dependency_paths for file %s: %s',
                        filename, absolute_dependency_paths)
  
          # Prepend @loader_path to all dependency paths.
          for dependency_path in dependency_paths:
              basename = os.path.basename(dependency_path)
              new_path = os.path.join(loader_path, basename)
  
              self.set_new_path(filename, dependency_path, new_path)
  
          logging.debug('Absolute_paths for %s: %s', filename, absolute_dependency_paths)
  
          # Since we have changed the dependency path, create a symlink so that the dependency path
          # points to a valid file. It's not guaranteed that this symlink will point to a valid
          # physical file, so the caller is responsible to make sure the physical file exists.
          for absolute_path in absolute_dependency_paths:
              lib_file_name = os.path.basename(absolute_path)
              relative_lib_path = os.path.join("..", lib_file_name, lib_file_name)
  
              # Create symlink in lib_bin_dir.
              symlink_path = os.path.join(lib_bin_dir, lib_file_name)
              if not os.path.exists(symlink_path):
                  logging.info('Creating symlink %s -> %s', symlink_path, relative_lib_path)
                  os.symlink(relative_lib_path, symlink_path)
  
          # Restore the file's mode.
          try:
              os.chmod(filename, original_mode)
          except OSError as e:
              logging.error('Unable to restore file %s mode', filename)
              raise
  
          return absolute_dependency_paths
  