1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
|
#!/usr/bin/env python3
# MACOS PACKAGING SCRIPT FOR NAEV
# This script should be run after compiling Naev.
# This script assumes a dependency must be bundled (with priority over any installations the end user might have) if either
# | it's in a known Homebrew or osxcross path (LOCAL_LIB_ROOTS below)
# | it's relative to the rpath (Meson subproject / embedded lib)
import argparse
import os
import shutil
import subprocess
LOCAL_LIB_ROOTS = ('/opt/local', '/usr/lib/osxcross', '/usr/local')
def main():
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--debug", help="Trace the script's actions.", action="store_true")
parser.add_argument("-n", "--nightly", help="Set this for nightly builds.", action="store_true")
parser.add_argument("-s", "--sourceroot", help="Set this to the repo location.", default=os.path.abspath('.'))
parser.add_argument("-b", "--buildroot", help="Set this to the build location.", default=os.path.abspath('build'))
parser.add_argument("-o", "--buildoutput", help="Set location for dist output.", default=os.path.abspath('dist'))
args = parser.parse_args()
trace.enabled = args.debug
app_path = f'{args.buildoutput}/out/Naev.app'
#Clean previous build.
trace(shutil.rmtree, app_path, ignore_errors=True)
# Build basic structure.
for subdir in 'MacOS', 'Resources', 'Frameworks':
trace(os.makedirs, f'{app_path}/Contents/{subdir}')
trace(shutil.copy, f'{args.buildroot}/Info.plist', f'{app_path}/Contents')
trace(shutil.copy, f'{args.sourceroot}/extras/macos/naev.icns', f'{app_path}/Contents/Resources')
# Gather Naev and dependencies.
trace(copy_with_deps, f'{args.buildroot}/naev', app_path, dest='Contents/MacOS')
# Strip headers, especially from the SDL2 framework.
trace(subprocess.check_call, ['find', app_path, '-name', 'Headers', '-prune', '-exec', 'rm', '-r', '{}', '+'])
# Install data.
trace(shutil.copytree, f'{args.sourceroot}/dat', f'{app_path}/Contents/Resources/dat')
trace(subprocess.check_call, [f'{args.sourceroot}/utils/package-po.sh',
'-b', args.buildroot, '-o', f'{app_path}/Contents/Resources'])
print(f'Successfully created {app_path}')
def trace(f, *args, **kwargs):
""" Run the given function on the given arguments. In debug mode, print the function call.
(Indent if we trace while running a traced function.) """
trace.depth += 1
try:
if trace.enabled:
# HACK: when tracing a function call, use unittest.mock to construct a pretty-printable version.
# HACK: when tracing subprocess shell commands, format them because it's unreadable otherwise.
if f.__module__ == 'subprocess' and len(args) == 1:
from subprocess import list2cmdline
print(f'[{trace.depth}]', '$', list2cmdline(args[0]))
else:
from unittest import mock
call = getattr(mock.call, f.__name__)(*args, **kwargs)
pretty = str(call).replace('call.', '', 1)
print(f'[{trace.depth}]', pretty)
return f(*args, **kwargs)
finally:
trace.depth -= 1
trace.depth = 0
trace.enabled = False
def copy_with_deps(bin_src, app_path, dest='Contents/Frameworks', exe_rpaths=None):
loader_path = os.path.dirname(bin_src)
bin_name = os.path.basename(bin_src)
bin_dst = os.path.join(app_path, dest, bin_name)
trace(shutil.copy, bin_src, bin_dst)
dylibs, rpaths = otool_results(bin_src)
# Now we have a "dylibs" list, where some entries may be "@rpath/" relative,
# and we have a "rpaths" list, where some entries may be "@loader_path/" relative.
# The "rpaths" list only matters when it comes from the Naev executable, so record that result
if exe_rpaths is None:
exe_rpaths = list(rpaths)
change_cmd = [host_program('install_name_tool')]
if bin_dst.endswith('/naev'):
change_cmd.extend(['-add_rpath', '@executable_path/../Frameworks'])
if bin_dst.endswith('.dylib'):
change_cmd.extend(['-id', f'@rpath/{bin_name}'])
for dylib in dylibs:
dylib_name = os.path.basename(dylib)
dylib_path = search_and_pop_rpath(dylib, exe_rpaths, loader_path)
if dylib_path:
if not os.path.exists(os.path.join(app_path, dest, dylib_name)):
trace(copy_with_deps, dylib_path, app_path)
change_cmd.extend(['-change', dylib, f'@rpath/{dylib_name}'])
for rpath in set(rpaths).difference(exe_rpaths):
change_cmd.extend(['-delete_rpath', rpath])
change_cmd.append(bin_dst)
trace(subprocess.check_call, change_cmd)
def otool_results(bin_src):
operands = {'LC_LOAD_DYLIB': [], 'LC_RPATH': []}
operand_list = unwanted = []
for line in subprocess.check_output([host_program('otool'), '-l', bin_src]).decode().splitlines():
line = line.lstrip()
if line.startswith('cmd '):
operand_list = operands.get(line[4:], unwanted)
elif line.startswith(('name ', 'path ')):
operand_list.append(line[5:].rsplit(' ', 2)[0]) # "name /some/path (offset 42)" -> "/some/path"
return operands['LC_LOAD_DYLIB'], operands['LC_RPATH']
def search_and_pop_rpath(dylib, rpaths, loader_path):
dylib_path = None
if dylib.startswith(LOCAL_LIB_ROOTS):
dylib_path = dylib
lib_dir = os.path.realpath(os.path.dirname(dylib))
for rpath in list(rpaths):
if os.path.realpath(rpath) == lib_dir:
rpaths.remove(rpath)
elif dylib.startswith('@rpath/'):
lib_base = dylib.replace('@rpath/', '', 1)
for rpath in list(rpaths):
trial_path = os.path.join(rpath.replace('@loader_path', loader_path), lib_base)
if os.path.exists(trial_path):
dylib_path = dylib_path or trial_path # We should bundle the first matching lib.
rpaths.remove(rpath)
return dylib_path
def host_program(name):
try:
return os.environ['HOST'] + '-' + name
except KeyError:
return name
if __name__ == '__main__':
main()
|