-
-
Save jakirkham/8f26e372e50a3d4a6ad87a43a463de83 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
#!/usr/bin/env python | |
""" | |
Inspect MACOSX deployment targets of conda packages | |
Uses `otool -l` and checks for LC_VERSION_MIN_MACOSX command to verify that dylibs target the right version of OS X | |
BY: Min RK | |
LICENSE: CC0, Public Domain | |
""" | |
from __future__ import print_function | |
from distutils.version import LooseVersion as V | |
import os | |
import sys | |
from subprocess import check_output | |
from conda.install import linked_data | |
def _otool_l(f): | |
"""Run and parse `otool -l` output on a file | |
Returns a list of command dicts, parsed from the | |
""" | |
output = check_output(['otool', '-l', f]).decode('utf8', 'replace').splitlines() | |
commands = [] | |
cmd = None | |
for line in output: | |
if not line.startswith((' ', '\t')): | |
# new command, save the last one | |
if cmd: | |
commands.append(cmd) | |
cmd = { | |
'name': line, | |
'data': {}, | |
} | |
elif cmd is not None: | |
parts = line.strip().split(None, 1) | |
if len(parts) == 2: | |
cmd['data'][parts[0]] = parts[1] | |
else: | |
# ignore these lines? | |
pass | |
return commands | |
def get_min_macosx_target(dylib): | |
"""Get the minimum macosx target for a dylib | |
Returns None if none can be found. | |
""" | |
commands = _otool_l(dylib) | |
min_macosx_commands = [ c for c in commands if c['data'].get('cmd') == 'LC_VERSION_MIN_MACOSX' ] | |
if not min_macosx_commands: | |
return | |
command = min_macosx_commands[0] | |
return command['data']['version'] | |
def inspect_macosx_targets(selected_packages=None, target_version='10.9', prefix=sys.prefix, | |
strict=False, verbose=False): | |
"""Inspect dylibs for compliance with a given MACOSX_DEPLOYMENT_TARGET | |
Any dylibs with deployment target greater than target_version will fail. | |
Default behavior is to treat dylibs with no target as okay. | |
If strict=True, dylibs with no target will fail. | |
""" | |
packages = linked_data(prefix) | |
target_v = V(target_version) | |
bad = {} | |
seen = set() | |
for pkg in packages.values(): | |
name = pkg['name'] | |
if selected_packages: | |
if name in selected_packages: | |
seen.add(name) | |
else: | |
continue | |
for f in pkg['files']: | |
if not f.endswith('.dylib'): | |
continue | |
path = os.path.join(prefix, f) | |
macosx_target = get_min_macosx_target(path) | |
if not macosx_target: | |
if verbose: | |
print("%s:%s - no target" % (name, path), file=sys.stderr) | |
if strict: | |
bad.setdefault(name, []) | |
bad[name].append((f, macosx_target)) | |
elif V(macosx_target) > target_v: | |
if verbose: | |
print("%s:%s - %s" % (name, path, macosx_target), file=sys.stderr) | |
bad.setdefault(name, []) | |
bad[name].append((f, macosx_target)) | |
elif verbose: | |
print("%s:%s - %s" % (name, path, macosx_target)) | |
if selected_packages: | |
missed = set(selected_packages).difference(seen) | |
if missed: | |
print("No such packages: %s" % ', '.join(sorted(missed)), file=sys.stderr) | |
return True | |
if bad: | |
for name, bad_files in bad.items(): | |
print("%s has %i files with bad targets:" % (name, len(bad_files)), file=sys.stderr) | |
for (f, macosx_target) in bad_files: | |
print(" %s: %s" % (f, macosx_target), file=sys.stderr) | |
return True | |
def main(): | |
import argparse | |
parser = argparse.ArgumentParser() | |
parser.add_argument(nargs='*', dest='packages', help='packages to inspect (default: all packages in the env)') | |
parser.add_argument('--target', default='10.9', | |
help='target threshold for minimum macOS version') | |
parser.add_argument('--prefix', default=os.environ.get('CONDA_PREFIX') or sys.prefix, | |
help='conda prefix in which to run (default: current env)') | |
parser.add_argument('--strict', action='store_true', | |
help='treat dylibs with no target as fail rather than pass') | |
parser.add_argument('-v', '--verbose', action='store_true', | |
help='verbose output') | |
opts = parser.parse_args() | |
bad = inspect_macosx_targets(opts.packages, opts.target, prefix=opts.prefix, | |
strict=opts.strict, verbose=opts.verbose) | |
if bad: | |
sys.exit(1) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment