Dylibs fix.m

From Octave
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

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