Skip to content

Instantly share code, notes, and snippets.

@snipsnipsnip
Last active August 24, 2019 16:06
Show Gist options
  • Save snipsnipsnip/1102438 to your computer and use it in GitHub Desktop.
Save snipsnipsnip/1102438 to your computer and use it in GitHub Desktop.
bindist.rb: resolves dylib dependencies of a Mach-O executable to make a distributable package
#!/usr/bin/ruby
=begin
== 概要
Usage: ruby bindist.rb [-f|-q] 動かない実行ファイル 手直ししたバイナリを置くディレクトリ
野良ビルドしたライブラリを使ってビルドしたMach-O実行ファイルを配布できるように手直しするスクリプト。
dylibの依存関係をotool -Lで調べて手元にコピーし、install_name_toolで再配線する。
システム標準と思われるライブラリ(/System や /usr/lib 以下)は配布先にもあると思うのでコピーしない。
== オプション
オプションを何もつけずに起動すると、依存関係を調べて、実行するであろうコマンドを出力する。
-f オプションを挟むと実際にファイルのコピーと再配線を行う。
-q オプションをつけると何も出力せずに実行する。
== 動作例
ruby bindist.rb -f a.out dist
とすると、dist/以下にa.outとその依存するライブラリが全てコピーされ、再配線される。
つまり、distフォルダを他のMacにコピーして動かせる状態になる。
distフォルダはなければ作成される。あれば内容が上書きされる。
=end
require 'fileutils'
class Bindist
def self.main(argv=ARGV)
if argv.size == 3 && argv[0] =~ /\A-f\z|\A--force\z/i
new.run(argv[1], argv[2], FileUtils::Verbose, NameTool.new(false), DependencyWalker.new(true))
elsif argv.size == 3 && argv[0] =~ /\A-q\z|\A--quiet\z/i
new.run(argv[1], argv[2], FileUtils, NameTool.new(nil), DependencyWalker.new(false))
elsif argv.size == 2
new.run(argv[0], argv[1], FileUtils::DryRun, NameTool.new(true), DependencyWalker.new(true))
else
abort "usage: #{File.basename $0} [-f|-q] executable destdir"
end
end
def run(exe, destdir, utils, nametool, walker)
deps = walker.walk_dependencies(exe)
utils.mkpath destdir
deps.each do |lib, libdeps|
name = File.basename(lib)
copied = File.join(destdir, name)
utils.install(lib, destdir)
lib == exe or nametool.rewrite_id(copied, make_relative(name))
libdeps.each do |dep|
nametool.rewrite_dependent_library(copied, dep, make_relative(dep))
end
end
end
private
def make_relative(path)
File.join("@executable_path", File.basename(path))
end
class DependencyWalker
def initialize(verbose)
@verbose = verbose
end
def verbose?
@verbose
end
def walk_dependencies(root)
build_graph(root) do |bin|
list_dependent_libraries(bin).reject {|lib| standard_library?(lib) }
end
end
private
def standard_library?(lib)
lib =~ %r{\A/usr/lib/|\A/System/}
end
def list_dependent_libraries(path)
warn "otool -L #{path}" if verbose?
list = `/usr/bin/otool -L '#{path}'`
$? == 0 or raise "otool failed: #$?"
list.scan(/^\s+(\/\S+)/).flatten
end
def build_graph(root)
graph = {}
stack = [root]
while node = stack.pop
next if graph.has_key?(node)
neighbors = yield(node)
graph[node] = neighbors
stack.concat neighbors
end
graph
end
end
class NameTool
def initialize(dry)
@dry = dry
end
def dry?
!!@dry
end
def verbose?
!@dry.nil?
end
def rewrite_id(lib, newid)
install_name_tool('-id', newid, lib)
end
def rewrite_dependent_library(lib, oldlib, newlib)
install_name_tool('-change', oldlib, newlib, lib)
end
private
def install_name_tool(*args)
warn "install_name_tool #{args.join(' ')}" if verbose?
return if dry?
system('/usr/bin/install_name_tool', *args) or raise "install_name_tool failed: #$?"
end
end
end
Bindist.main if $0 == __FILE__
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment