forked from sammy-tri/drake
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a module for parsing
otool
output
Create a new Python module for parsing the output of (macOS) `otool` into structured data. Use this in `installer.py`. The new module supports extraction of load commands (`otool -l`, only the "Load Command N" blocks) and linked libraries (`otool -L`), and should be slightly more robust than the prior parsing code, particularly the old load command parsing which acted on any "path" key without regard to the type of command to which the key belonged. This will also allow this logic to be reused for other tools that need to perform RPATH manipulations.
- Loading branch information
1 parent
bd5d534
commit 8bc0195
Showing
4 changed files
with
190 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Empty Python module `__init__`, required to make this a module. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
""" | ||
Pythonic wrappers for macOS `otool`. | ||
""" | ||
|
||
import os | ||
import re | ||
import subprocess | ||
|
||
from collections import namedtuple | ||
|
||
Library = namedtuple('Library', [ | ||
'basename', | ||
'path', | ||
'version_compat', | ||
'version_current', | ||
]) | ||
|
||
# Known Load command keys that contain spaces; DO NOT MODIFY at runtime. | ||
_load_command_keys = ( | ||
'time stamp', | ||
'current version', | ||
'compatibility version', | ||
) | ||
|
||
|
||
def _join(proc, cmd='otool'): | ||
""" | ||
Wait for process `proc` to terminate, and raise an exception if it did not | ||
exit successfully (i.e. gave a non-zero exit code). | ||
""" | ||
retcode = proc.wait(timeout=30) | ||
if retcode: | ||
raise CalledProcessError(retcode, cmd) | ||
|
||
|
||
def _split_load_command(line): | ||
""" | ||
Splits a load command line into a key, value pair. Handles known key names | ||
that contain spaces. | ||
""" | ||
for key in _load_command_keys: | ||
if line.startswith(f'{key} '): | ||
return [key, line[len(key):].lstrip()] | ||
|
||
return line.split(' ', 1) | ||
|
||
|
||
def load_commands(path): | ||
""" | ||
Obtains the load commands of a Mach-O binary. Returns a list of commands, | ||
where each command is a dictionary describing the command. The key 'cmd' | ||
is always present and describes the type of command. The command type will | ||
determine what other keys are present. | ||
For the most part, values (e.g. time stamps) are not translated. As a | ||
partial exception, strings of the form 'value (offset X)' are split, and | ||
the offset is stored as 'Y:offset', where 'Y' is the corresponding key | ||
name. As another exception, 'cmdsize' is translated to ``int``. | ||
""" | ||
commands = [] | ||
command = None | ||
|
||
proc = subprocess.Popen( | ||
['otool', '-l', path], | ||
stdout=subprocess.PIPE, | ||
text=True, | ||
) | ||
|
||
for line in proc.stdout: | ||
# Output looks like:: | ||
# | ||
# Load command N | ||
# cmd LC_FOO | ||
# cmdsize 64 | ||
# key1 value1 (offset 16) | ||
# key2 value2 | ||
# extended key value | ||
# Section | ||
# key1 value1 | ||
# key2 value2 | ||
# | ||
# Key names may or may not be indented, and some key names contain | ||
# spaces. Most values are aligned to a particular (but varying) column, | ||
# but long key names may change this column. | ||
if line.startswith('Load command'): | ||
if command is not None and len(command): | ||
commands.append(command) | ||
|
||
command = {} | ||
|
||
elif line == 'Section\n': | ||
if command is not None and len(command): | ||
commands.append(command) | ||
|
||
command = None | ||
|
||
elif command is not None: | ||
kv = _split_load_command(line.strip()) | ||
if len(kv) == 2: | ||
m = re.match('^(.*) [(]offset ([0-9]+)[)]$', kv[1].strip()) | ||
if m is None: | ||
if kv[0] == 'cmdsize': | ||
command[kv[0]] = int(kv[1].strip()) | ||
else: | ||
command[kv[0]] = kv[1].strip() | ||
else: | ||
command[kv[0]] = m.group(1).strip() | ||
command[f'{kv[0]}:offset'] = int(m.group(2)) | ||
|
||
_join(proc) | ||
|
||
return commands | ||
|
||
|
||
def linked_libraries(path): | ||
""" | ||
Obtains the set of shared libraries referenced by a Mach-O binary. Returns | ||
a list of Library objects. | ||
""" | ||
libs = [] | ||
|
||
proc = subprocess.Popen( | ||
['otool', '-L', path], | ||
stdout=subprocess.PIPE, | ||
text=True, | ||
) | ||
|
||
for line in proc.stdout: | ||
# Output looks like (note that actual indent uses '\t'):: | ||
# | ||
# /path/to/input.dylib | ||
# @rpath/libfoo.0.dylib <version> | ||
# /usr/lib/libbar.5.dylib <version> | ||
# ... | ||
# | ||
# <version> looks like '(compatibility version 1.0.0, ' | ||
# 'current version 5.0.0)'. | ||
m = re.match('^\t(.*)[(]([^)]+)[)]\\s*$', line) | ||
if m is not None: | ||
path = m.group(1).strip() | ||
|
||
m = re.match('^compatibility version (.*), current version (.*)$', | ||
m.group(2).strip()) | ||
if m is not None: | ||
compat = m.group(1) | ||
current = m.group(2) | ||
else: | ||
compat = None | ||
current = None | ||
|
||
libs.append( | ||
Library( | ||
path=path, | ||
basename=os.path.basename(path), | ||
version_compat=compat, | ||
version_current=current)) | ||
|
||
_join(proc) | ||
|
||
return libs |