1#! /usr/bin/env ruby
2#
3# This script makes the Couchbase Server binaries self-contained by locating all nonstandard
4# external dynamic library dependencies, copying those libraries into "lib/", and fixing up the
5# imports to point to the copied libraries.
6#
7# It must be called with the cwd set to the root directory of the installation ("couchbase-core").
8
9
10require "pathname"
11
12LibraryDir = Pathname.new("lib")
13BinDir = Pathname.new("bin")
14
15def log (message)
16  #   puts message       # Uncomment for verbose logging
17end
18
19# Returns the libraries imported by the binary at 'path', as an array of Pathnames.
20def get_imports (path)
21  imports = []
22  puts "path: #{path}"
23  for line in `otool -L '#{path}'`.split("\n")
24    if line =~ /^\t(.*)\s*\(.*\)$/
25      import = Pathname.new($1.rstrip)
26      if import.basename != path.basename
27        imports << import
28      end
29    end
30  end
31  return imports
32end
33
34
35# Edits the binary at 'libpath' to change its import of 'import' to 'newimport'.
36def change_import (libpath, import, newimport)
37  log "\tchange_import called with libpath: #{libpath}, import: #{import}, newimport: #{newimport}"
38  return  if newimport == import
39  log "\tChange import #{import} to #{newimport}"
40  unless system("install_name_tool", "-change", import, newimport, libpath)
41    fail "install_name_tool failed"
42  end
43end
44
45
46# Copies a library from 'src' into 'lib/', and recursively processes its imports.
47def copy_lib (src, loaded_from)
48  return src  if src.to_s.start_with?("lib/")
49
50  dst = LibraryDir + src.basename
51  if dst.exist?  # already been copied
52    return dst
53  end
54
55  if src.dirname.to_s == "@loader_path"
56    src = loaded_from.dirname + src.basename
57  end
58  fail "bad path #{src}"  unless src.absolute?
59
60  log "\tCopying #{src} --> #{dst}"
61  unless system("cp", src.to_s, dst.to_s)
62    fail "cp failed on #{src}"
63  end
64  dst.chmod(0644)  # Make it writable so we can change its imports
65
66  process(dst, src)
67  return dst
68end
69
70
71# Fixes up the binary at 'file' by locating external library dependencies and copying those
72# libraries to "lib/".
73# If 'original_path' is given, it is the path from which 'file' was copied; this is needed
74# for resolution of '@loader_path'-relative imports.
75def process (file, original_path =nil)
76  log "-- #{file} ..."
77  for import in get_imports(file) do
78    path = import.to_s
79    unless path.start_with?("/usr/lib/") || path.start_with?("/System/")
80      dst = copy_lib(import, (original_path || file))
81      unless dst.absolute?
82        dst = '@loader_path/' + dst.relative_path_from(file.dirname).to_s
83      end
84      change_import(file.to_s, import.to_s, dst.to_s)
85    end
86  end
87  log "\tend #{file}"
88end
89
90
91# Calls process() on every dylib in the directory tree rooted at 'dir'.
92def process_libs_in_tree (dir)
93  dir.children.each do |file|
94    if file.directory?
95      process_libs_in_tree file
96    elsif (file.extname == ".dylib" || file.extname == ".so") && file.ftype == "file"
97      process(file)
98    end
99  end
100end
101  
102def process_binaries_in_tree (dir)
103  dir.children.each do |file|
104    if file.directory?
105      process_binaries_in_tree file
106    elsif file.ftype == "file" && file.executable?
107      File.open(file, 'r') do |f|
108        if f.getc == '#' && f.getc == '!'
109          log "Skipping script file #{file}."
110        else
111          process(file)
112        end
113      end
114    else
115      log "Skipping #{file}."
116    end
117  end
118end
119
120
121### OK, here's the main code:
122
123puts "Fixing library imports in #{BinDir} ..."
124process_binaries_in_tree BinDir
125
126puts "\nFixing library imports in #{LibraryDir} ..."
127process_libs_in_tree LibraryDir
128
129puts "\nDone fixing library imports!"
130