Dylibs fix.m

From Octave
Jump to: navigation, search

An m-file function used in support of creating a MacOS X App bundle.

## Copyright (C) 2012 Ben Abbott
## 
## 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 3 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 Octave; see the file COPYING.  If not, see
## <http://www.gnu.org/licenses/>.

## -*- texinfo -*-
## @deftypefn {Function File} fix_dylibs (@var{exebin}, @var{libdir}, @var{dryrun})
## Make the executable and dynamic libraries relocatable. The inputs are;
##
## @table
## @item @var{exebin}
## The full file name of the executable binary for the App bundle.
## The default is @code{bin/Octave-3.7.0+};
## @item @var{libdir}
## The full path to the directory containing the App bundle's dynamic
## libraries.  The default is @code{lib}.
## @item @var{dryrun}
## If @var{true}, the @code{install_name_tool} commands are printed to
## the command line and are not executed (i.e. the install names and paths
## to the dependent libraries are not changed).  The default is @var{true}.
## @end table
##
## Using @code{install_name_tool} the portion of the built in
## dynamic library paths external to the App bundles are replaced with
## the token @code{@@executable_path}.
## Ths will modify the paths in both the @var{exebin} and the the dynamic
## libraries and allow the application to be relocated with no risk
## that the binary executable and libraries are unable to locate the
## dynamic libraries they depend upon.
##
## In addition to correcting the location information for the dependent's
## of @var{exebin}, the @var{libdir} is searched recursively to locate all
## dynamic libraries present in the App bundle.  Both their install names
## and the locations of their dependents will also be fixed.
##
## Using the default, this script expects to find a @code{bin} and
## @code{lib} directory in the current working directory.  
##
## @center BACKGROUND INFORMATION BELOW @end center
##
## MacOS X supports the use of tokens to permit executable files and dynamic
## libraries to be relocated to a different path.  This is an essential
## feature for MacOS X's App bundles.  The three tokens supporte are below;
##
## @table @code
## @item @@executable_path
##   This token expands to the path of the executable application that is
##   loading the dynamic library.  Using this toek requires the location of
##   the library remains fixed relative to the binary executable.
## @item @@loader_path
##   This token expands to the path of binary loading the dynamic library.
##   The loader may be the binary application, a library, or a framework.
##   Using this token requires the location of the library remains fixed
##   relative to the loader.
## @item @@rpath
##   Searches a list of locations for the dynamic library.  @rpath is the
##   most flexible of these tokens.  When set in the install name of a
##   dynamic library, that library can be used in any applications,
##   frameworks or any other type of project without needing further
##   modification.  The @var{rpath} may be modified for a binary by using
##   the @code{otool} utility.
##
##   @example
##   otool -add_rpath @var{path_to_binary} @var{name_of_binary}
##   @end example
##
##   The @var{path_to_binary} may use either the @code{@@executable_path}
##   or @code{@@loader_path} tokens.
##
##   As it isn't clear how to check the entires of the @code{@@rpath} token.
##   Is it possible that dynamic libraries built by package managers
##   such as MacPorts or Fink have @code{@@rpath} entries to 
##   @code{/opt/loca/lib/..} or @code{/sw/lib/...}?  As a precaution, this
##   token can be avoided.
## @end table
##
## A hypothetical bundle, @var{my_exe.app}, will be used for demonstration.
## In this example, @var{my_exe} depends upon @var{libfoo.dyld} and
## @var{libbar.dyld}, and @var{libfoo.dyld} also depends upon @var{libbar.dyld}.
##
## @example
## ex.app/Contents/Resources/bin/@var{my_exe}
## ex.app/Contents/Resources/lib/@var{libfoo.dyld}
## ex.app/Contents/Resources/lib/@var{libbar.dyld}
## @end example
##
## The @code{@@executable_path} token represents the path to the executable, @var{my_exe}.
##
## @example
## @@executable_path = @code{/Applications/ex.app/Contents/Resources/bin}
## @end example
##
## To properly relocate the dynamic libraries the install name ID for each
## dynamic library must be set correctly, and the binary files which depend
## upon dynamic libraries need to be told where to find them.
##
## For this hypothetical example, and using the @code{@@executable_path} token,
## the commands below will properly relocate the dynamic libraries and fix their
## install name IDs.
##
## @example
## install_name_tool -change /Applications/ex.app/Contents/Resources/lib/@var{libfoo.dyld} \
##                           @@executable_path/../lib/@var{libfoo.dyld} \
##                           bin/@var{my_exe}
## install_name_tool -change /Applications/ex.app/Contents/Resources/lib/@var{libbar.dyld} \
##                           @@executable_path/../lib/@var{libbar.dyld} \
##                           bin/@var{my_exe}
## install_name_tool -change /Applications/ex.app/Contents/Resources/lib/@var{libbar.dyld} \
##                           @@executable_path/../lib/@var{libbar.dyld} \
##                           ../lib/@var{libfoo.dyld}
## install_name_tool -id @@executable_path/../lib/@var{libfoo.dyld} lib/@var{libfoo.dyld}
## install_name_tool -id @@executable_path/../lib/@var{libbar.dyld} lib/@var{libbar.dyld}
## @end example
##
## The @code{otool} utility may be used check the install name of a binary
## application. Replace @var{binary_name} with the actual path and name of
## the binary.
##
## @example
## otool -D @var{binary_name}
## @end example
##
## The @code{otool} utility may be also be used check the locations of its
## dependent libraries.  Again, replace @var{binary_name} with the actual path
## and name of the binary.  The first entry is the binary's install name.
##
## @example
## otool -L @var{binary_name}
## @end example
##
## @end deftypefn

## Author: Ben Abbott <bpabbott@mac.com>
## Created: 2012-06-26

## NOTE - It is possible to avoid a recursive search down the directory tree, but this would
##        require that the bundle's directory tree accurately represent the part above the
##        @executable_path.  With the approach used, the stored dylib locations are not
##        relevant.  Thus, their paths can be mangled in any way and this script will fix it.

function dylibs_fix (BINARY = "bin/octave-3.7.0+", LIBDIR = "lib", dryrun = true)
  if (ischar (BINARY))
    BINARY = {BINARY};
  endif
  only_binary = false;
  pager_state(1) = page_output_immediately (true);
  pager_state(2) = page_screen_output (false);
  unwind_protect
    dylibs = dylibs_find (LIBDIR);
    [~, n] = sort ({dylibs.name});
    dylibs = dylibs (n);
    printf ("%d dynamic libraries found.\n", numel (dylibs));
    executable_path = file_location (BINARY{1});
    for b = 1:numel(BINARY)
      cmds = fix_deps (executable_path, BINARY{b}, dylibs);
      for c = 1:numel(cmds)
        printf ("%s", cmds{c})
        [status, output] = system (cmds{c});
        if (! status == 0)
          error (output);
        endif
      endfor
    endfor
    if (! only_binary)
      for d = 1:numel(dylibs)
        full_name = strcat (dylibs(d).location, filesep (), dylibs(d).name);
        if (! dylibs(d).islink)
          ## Don't fix symbolic links
          cmd = fix_id (executable_path, full_name);
          cmds = fix_deps (executable_path, dylibs(d).name, dylibs);
          cmds = [cmd;cmds];
          if (dryrun)
            printf ("%s", cmds{:})
          else
            for c = 1:numel(cmds)
              printf ("%s", cmds{c})
              [status, output] = system (cmds{c});
              if (! status == 0)
                error (output);
              endif
            endfor
          endif
        endif
      endfor
    endif
  unwind_protect_cleanup
    page_output_immediately (pager_state(1));
    page_screen_output (pager_state(2));
  end_unwind_protect
endfunction

function cmds = fix_deps (executable_path, binary, dylibs)
  [~, name, ext] = fileparts (binary);
  binary = strcat (name, ext);
  m = find (strcmp ({dylibs.name}, binary));
  if (isempty (m))
    ## If not in dylib list, it must be an executable
    full_binary = strcat (executable_path, filesep (), binary);
    is_link = false;
  else
    full_binary = strcat (dylibs(m).location, filesep (), binary);
    is_link = dylibs(m).islink;
  endif
  if (! is_link)
    [deps, full_deps] = dylibs_get_deps (full_binary);
    cmds = cell (numel (deps), 1);
    for n = 1:numel(deps)
      m = find (strcmp ({dylibs.name}, deps{n}), 1);
      if (! isempty (m))
        ## Don't run install_tool_name on symbolic links
        full_name = strcat (dylibs(m).location, filesep (), dylibs(m).name);
        tokenized_dep = tokenized_path (executable_path, full_name);
        cmds{n} = sprintf ("install_name_tool -change '%s' '%s' '%s'\n\n",
                           full_deps{n},
                           tokenized_dep,
                           full_binary);
      else
        warning ('dylibs_fix: Missing dynamic library "%s" needed by "%s"',
                 deps{n}, binary)
      endif
    endfor
    ## Missing dynamic libraries result in empty cmds
    cmds(cellfun(@isempty,cmds,"UniforOutput",true)) = [];
  else
    cmds = {};
  endif
endfunction

function cmd = fix_id (executable_path, full_binary)
  try
    if (dylibs_isdylib (full_binary))
      tokenized_binary = tokenized_path (executable_path, full_binary);
      cmd = sprintf ("install_name_tool -id '%s' '%s'\n",
                     tokenized_binary,
                     full_binary);
    endif
  catch
    save fix_id.mat
    rethrow (lasterror ())
  end_try_catch
endfunction

function tpath = tokenized_path (executable_path, binary)
  binary_path = file_location (binary);
  [~, name, ext] = fileparts (binary);
  name = strcat (name, ext);
  binary = strcat (binary_path, filesep (), name);
  n = min (numel (binary_path), numel (executable_path));
  n = binary_path(1:n) == executable_path(1:n);
  n = find (n == 0, 1, "first") - 1;
  shared_path = binary_path(1:n);
  extra_dirs = numel (findstr (executable_path(n+1:end), filesep ()));
  exe2bin = repmat (strcat ("..", filesep ()), [1, extra_dirs+1]);
  tpath = strcat ("@executable_path", filesep (), exe2bin, binary_path(n+1:end),
                  filesep (), name);
endfunction

function loc = file_location (file)
  path2file = fileparts (file);
  cwd = pwd ();
  unwind_protect
    if (! isempty (path2file))
      cd (path2file)
    endif
    loc = pwd ();
  unwind_protect_cleanup
    cd (cwd);
  end_unwind_protect
endfunction