Skip to content

Commit ea570f2

Browse files
committed
FEAT: add OO API, remove everything else from main namespace.
1 parent 779574a commit ea570f2

14 files changed

+428
-46
lines changed

README.rst

+22-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,31 @@ Examples::
1616
Internally, machotools is written as a library so that it can be used within
1717
bigger tools, but the API is currently in-flux until the first 1.0 release.
1818

19+
Example::
20+
21+
from machotools import rewriter_factory
22+
23+
rewriter = rewriter_factory("foo.dylib")
24+
print rewriter.dependencies
25+
# install_name property only available if rewriter is a DylibRewriter
26+
print rewriter.install_name
27+
print rewriter.rpaths
28+
29+
rewriter.install_name = "bar.dylib"
30+
# Changes are not actually written until you call commit()
31+
rewriter.commit()
32+
33+
Main features:
34+
35+
- ability to query/change rpath
36+
- ability to query/change the install name
37+
- ability to query/change the dependencies
38+
- modifications are safe against crash/interruption as files are never
39+
modified in place.
40+
1941
Development happens on `github <http://github.com/enthought/machotools>`_
2042

2143
TODO:
2244

2345
- support for multi arch
2446
- more detailed output for list_libraries (including versioning info)
25-
- add OO API

dev_requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
coverage
2+
tox

machotools/__init__.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# Copyright (c) 2013 by Enthought, Ltd.
22
# All rights reserved.
33

4-
# This is the public API
5-
from machotools.dependency import dependencies, change_dependency
6-
from machotools.misc import change_install_name, install_name
7-
from machotools.rpath import add_rpaths, list_rpaths
4+
from machotools.macho_rewriter import BundleRewriter, DylibRewriter, \
5+
ExecutableRewriter, rewriter_factory
6+
7+
# Silent pyflakes vim plugin
8+
__all__ = [
9+
"BundleRewriter", "DylibRewriter", "ExecutableRewriter",
10+
"rewriter_factory"
11+
]

machotools/dependency.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import os
21
import re
32

43
import macholib
@@ -35,9 +34,12 @@ def dependencies(filename):
3534
dependency_names: seq
3635
dependency_names[i] is the list of dependencies for the i-th header.
3736
"""
37+
m = macholib.MachO.MachO(filename)
38+
return _list_dependencies_macho(m)
39+
40+
def _list_dependencies_macho(m):
3841
ret = []
3942

40-
m = macholib.MachO.MachO(filename)
4143
for header in m.headers:
4244
this_ret = []
4345
for load_command, dylib_command, data in header.commands:

machotools/detect.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
from macholib.MachO import MachO
22

3-
def _is_macho_type(path, macho_type):
3+
def detect_macho_type(path):
4+
"""
5+
Returns None if not a mach-o.
6+
"""
47
try:
58
p = MachO(path)
69
except ValueError as e:
710
# Grrr, why isn't macholib raising proper exceptions...
811
assert str(e).startswith("Unknown Mach-O")
9-
return False
12+
return None
1013
else:
1114
if len(p.headers) < 1:
1215
raise ValueError("No headers in the mach-o file ?")
1316
else:
14-
return p.headers[0].filetype == macho_type
17+
return p.headers[0].filetype
1518

1619
def is_macho(path):
1720
"""Return True if the given path is a Mach-O binary."""
1821
try:
19-
p = MachO(path)
22+
MachO(path)
2023
return True
2124
except ValueError as e:
2225
# Grrr, why isn't macholib raising proper exceptions...
@@ -25,12 +28,12 @@ def is_macho(path):
2528

2629
def is_executable(path):
2730
"""Return True if the given file is a mach-o file of type 'execute'."""
28-
return _is_macho_type(path, "execute")
31+
return detect_macho_type(path) == "execute"
2932

3033
def is_dylib(path):
3134
"""Return True if the given file is a mach-o file of type 'execute'."""
32-
return _is_macho_type(path, "dylib")
35+
return detect_macho_type(path) == "dylib"
3336

3437
def is_bundle(path):
3538
"""Return True if the given file is a mach-o file of type 'bundle'."""
36-
return _is_macho_type(path, "bundle")
39+
return detect_macho_type(path) == "bundle"

machotools/errors.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class MachoError(Exception):
2+
pass

machotools/macho_rewriter.py

+208
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import re
2+
3+
import macholib
4+
5+
from .errors import MachoError
6+
from .dependency import _list_dependencies_macho, _change_command_data_inplace, \
7+
_find_lc_dylib_command
8+
from .detect import detect_macho_type
9+
from .misc import _install_name_macho, _change_id_dylib_command
10+
from .rpath import _add_rpath_to_header, _list_rpaths_macho
11+
from .utils import rstrip_null_bytes, safe_update
12+
13+
class _MachoRewriter(object):
14+
"""
15+
Macho rewriters can be used to query and change mach-o properties relevant
16+
for relocatability.
17+
18+
Concretely, you can query/modify the following:
19+
20+
- rpaths sections
21+
- dependencies
22+
23+
See also
24+
--------
25+
rewriter_factory which instanciates the right rewriter by auto-guessing the
26+
mach-o type.
27+
"""
28+
def __init__(self, filename):
29+
self.filename = filename
30+
31+
self._m = macholib.MachO.MachO(filename)
32+
if len(self._m.headers) == 0:
33+
raise MachoError("No header found ?")
34+
elif len(self._m.headers) > 1:
35+
raise MachoError("Universal binaries not yet supported")
36+
37+
self._rpaths = _list_rpaths_macho(self._m)[0]
38+
self._dependencies = _list_dependencies_macho(self._m)[0]
39+
40+
def __enter__(self):
41+
return self
42+
43+
def __exit__(self, *a, **kw):
44+
self.commit()
45+
46+
def commit(self):
47+
def writer(f):
48+
f.seek(0)
49+
self._m.headers[0].write(f)
50+
safe_update(self.filename, writer, "wb")
51+
52+
@property
53+
def rpaths(self):
54+
"""
55+
This is the list of defined rpaths.
56+
57+
Note
58+
----
59+
This includes the list of uncommitted changes.
60+
"""
61+
return self._rpaths
62+
63+
#----------
64+
# rpath API
65+
#----------
66+
def extend_rpaths(self, new_rpaths):
67+
"""
68+
Extend the existing set of rpaths with the given list.
69+
70+
Parameters
71+
----------
72+
new_rpaths: seq
73+
List of rpaths (i.e. list of strings).
74+
75+
Note
76+
----
77+
The binary is not actually updated intil the sync method has been
78+
called.
79+
"""
80+
header = self._m.headers[0]
81+
for rpath in new_rpaths:
82+
self._rpaths.append(rpath)
83+
_add_rpath_to_header(header, rpath)
84+
85+
def append_rpath(self, new_rpath):
86+
"""
87+
Append the given rpath to the existing set of rpaths.
88+
89+
Parameters
90+
----------
91+
new_rpath: str
92+
The new rpath.
93+
94+
Note
95+
----
96+
The binary is not actually updated intil the sync method has been
97+
called.
98+
"""
99+
header = self._m.headers[0]
100+
self._rpaths.append(new_rpath)
101+
_add_rpath_to_header(header, new_rpath)
102+
103+
def append_rpath_if_not_exists(self, new_rpath):
104+
"""
105+
Append the given rpath to the existing set of rpaths, but only if it
106+
does not already defined in the binary.
107+
108+
Parameters
109+
----------
110+
new_rpath: str
111+
The new rpath.
112+
113+
Note
114+
----
115+
The binary is not actually updated until the sync method has been
116+
called.
117+
"""
118+
header = self._m.headers[0]
119+
if not new_rpath in self._rpaths:
120+
_add_rpath_to_header(header, new_rpath)
121+
122+
#-----------------
123+
# dependencies API
124+
#-----------------
125+
@property
126+
def dependencies(self):
127+
"""
128+
The list of dependencies.
129+
130+
Note
131+
----
132+
This includes the list of uncommitted changes.
133+
"""
134+
return self._dependencies
135+
136+
def change_dependency(self, old_dependency_pattern, new_dependency,
137+
ignore_error=True):
138+
"""
139+
Change the dependency matching the given pattern to the new dependency
140+
name.
141+
142+
Parameters
143+
----------
144+
old_dependency_pattern: str
145+
Regex pattern to match against
146+
new_dependency: str
147+
New dependency name to replace with.
148+
ignore_error: bool
149+
If true, do not raise an exception of no dependency has been
150+
changed.
151+
"""
152+
r_old_dependency = re.compile(old_dependency_pattern)
153+
old_dependencies = self._dependencies[:]
154+
155+
header = self._m.headers[0]
156+
157+
i_dependency = 0
158+
for command_index, (load_command, dylib_command, data) in \
159+
_find_lc_dylib_command(header, macholib.mach_o.LC_LOAD_DYLIB):
160+
161+
name = rstrip_null_bytes(data)
162+
m = r_old_dependency.search(name)
163+
if m:
164+
_change_command_data_inplace(header, command_index,
165+
(load_command, dylib_command, data), new_dependency)
166+
self._dependencies[i_dependency] = new_dependency
167+
168+
i_dependency += 1
169+
170+
if not ignore_error and old_dependencies != self._dependencies:
171+
raise MachoError("Pattern {0} not found in the list of dependencies".
172+
format(old_dependency_pattern))
173+
174+
class ExecutableRewriter(_MachoRewriter):
175+
pass
176+
177+
class BundleRewriter(_MachoRewriter):
178+
pass
179+
180+
class DylibRewriter(_MachoRewriter):
181+
def __init__(self, filename):
182+
super(DylibRewriter, self).__init__(filename)
183+
184+
if not self._m.headers[0].filetype == "dylib":
185+
raise MachoError("file {0} is not a dylib".format(filename))
186+
187+
self._install_name = _install_name_macho(self._m)[0]
188+
189+
@property
190+
def install_name(self):
191+
return self._install_name
192+
193+
@install_name.setter
194+
def install_name(self, new_install_name):
195+
_change_id_dylib_command(self._m.headers[0], new_install_name)
196+
197+
self._install_name = new_install_name
198+
199+
def rewriter_factory(filename):
200+
macho_type = detect_macho_type(filename)
201+
if macho_type == "dylib":
202+
return DylibRewriter(filename)
203+
elif macho_type == "execute":
204+
return ExecutableRewriter(filename)
205+
elif macho_type == "bundle":
206+
return BundleRewriter(filename)
207+
else:
208+
raise MachoError("file {0} is not a mach-o file !".format(filename))

machotools/misc.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99

1010
def install_name(filename):
1111
"""Returns the install name of a mach-o dylib file."""
12+
m = macholib.MachO.MachO(filename)
13+
return _install_name_macho(m)
14+
15+
def _install_name_macho(m):
1216
ret = []
1317

14-
m = macholib.MachO.MachO(filename)
1518
for header in m.headers:
1619
install_names = []
1720
for command in header.commands:
@@ -51,15 +54,15 @@ def change_install_name(filename, new_install_name):
5154
"""
5255
m = macholib.MachO.MachO(filename)
5356
for header in m.headers:
54-
_change_dylib_command(header, new_install_name)
57+
_change_id_dylib_command(header, new_install_name)
5558

5659
def writer(f):
5760
for header in m.headers:
5861
f.seek(0)
5962
header.write(f)
6063
safe_update(filename, writer, "wb")
6164

62-
def _change_dylib_command(header, new_install_name):
65+
def _change_id_dylib_command(header, new_install_name):
6366
command_index, command_tuple = _find_lc_id_dylib(header)
6467
_change_command_data_inplace(header, command_index, command_tuple, new_install_name)
6568

machotools/rpath.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ def list_rpaths(filename):
2222
filename: str
2323
The path to the mach-o binary file to look at
2424
"""
25+
m = macholib.MachO.MachO(filename)
26+
return _list_rpaths_macho(m)
27+
28+
def _list_rpaths_macho(m):
2529
rpaths = []
2630

27-
m = macholib.MachO.MachO(filename)
2831
for header in m.headers:
2932
header_rpaths = []
3033
rpath_commands = [command for command in header.commands if

0 commit comments

Comments
 (0)