#!/usr/bin/env python candidate_paths = "bin obs-plugins data".split() plist_path = "../cmake/osxbundle/Info.plist" icon_path = "../cmake/osxbundle/obs.icns" run_path = "../cmake/osxbundle/obslaunch.sh" #not copied blacklist = """/usr /System""".split() #copied whitelist = """/usr/local""".split() # # # from sys import argv from glob import glob from subprocess import check_output, call from collections import namedtuple from shutil import copy, copytree, rmtree from os import makedirs, rename, walk, path as ospath import plistlib import argparse def _str_to_bool(s): """Convert string to bool (in argparse context).""" if s.lower() not in ['true', 'false']: raise ValueError('Need bool; got %r' % s) return {'true': True, 'false': False}[s.lower()] def add_boolean_argument(parser, name, default=False): """Add a boolean argument to an ArgumentParser instance.""" group = parser.add_mutually_exclusive_group() group.add_argument( '--' + name, nargs='?', default=default, const=True, type=_str_to_bool) group.add_argument('--no' + name, dest=name, action='store_false') parser = argparse.ArgumentParser(description='obs-studio package util') parser.add_argument('-d', '--base-dir', dest='dir', default='rundir/RelWithDebInfo') parser.add_argument('-n', '--build-number', dest='build_number', default='0') parser.add_argument('-k', '--public-key', dest='public_key', default='OBSPublicDSAKey.pem') parser.add_argument('-f', '--sparkle-framework', dest='sparkle', default=None) parser.add_argument('-b', '--base-url', dest='base_url', default='https://obsproject.com/osx_update') parser.add_argument('-u', '--user', dest='user', default='jp9000') parser.add_argument('-c', '--channel', dest='channel', default='master') add_boolean_argument(parser, 'stable', default=False) parser.add_argument('-p', '--prefix', dest='prefix', default='') args = parser.parse_args() def cmd(cmd): import subprocess import shlex return subprocess.check_output(shlex.split(cmd)).rstrip('\r\n') LibTarget = namedtuple("LibTarget", ("path", "external", "copy_as")) inspect = list() inspected = set() build_path = args.dir build_path = build_path.replace("\\ ", " ") def add(name, external=False, copy_as=None): if external and copy_as is None: copy_as = name.split("/")[-1] if name[0] != "/": name = build_path+"/"+name t = LibTarget(name, external, copy_as) if t in inspected: return inspect.append(t) inspected.add(t) for i in candidate_paths: print("Checking " + i) for root, dirs, files in walk(build_path+"/"+i): for file_ in files: if ".ini" in file_: continue if ".png" in file_: continue if ".effect" in file_: continue if ".py" in file_: continue if ".json" in file_: continue path = root + "/" + file_ try: out = check_output("{0}otool -L '{1}'".format(args.prefix, path), shell=True, universal_newlines=True) if "is not an object file" in out: continue except: continue rel_path = path[len(build_path)+1:] print(repr(path), repr(rel_path)) add(rel_path) def add_plugins(path, replace): for img in glob(path.replace( "lib/QtCore.framework/Versions/5/QtCore", "plugins/%s/*"%replace).replace( "Library/Frameworks/QtCore.framework/Versions/5/QtCore", "share/qt5/plugins/%s/*"%replace)): if "_debug" in img: continue add(img, True, img.split("plugins/")[-1]) actual_sparkle_path = '@loader_path/Frameworks/Sparkle.framework/Versions/A/Sparkle' while inspect: target = inspect.pop() print("inspecting", repr(target)) path = target.path if path[0] == "@": continue out = check_output("{0}otool -L '{1}'".format(args.prefix, path), shell=True, universal_newlines=True) if "QtCore" in path: add_plugins(path, "platforms") add_plugins(path, "imageformats") add_plugins(path, "accessible") add_plugins(path, "styles") for line in out.split("\n")[1:]: new = line.strip().split(" (")[0] if '@' in new and "sparkle.framework" in new.lower(): actual_sparkle_path = new print "Using sparkle path:", repr(actual_sparkle_path) if not new or new[0] == "@" or new.endswith(path.split("/")[-1]): continue whitelisted = False for i in whitelist: if new.startswith(i): whitelisted = True if not whitelisted: blacklisted = False for i in blacklist: if new.startswith(i): blacklisted = True break if blacklisted: continue add(new, True) changes = list() for path, external, copy_as in inspected: if not external: continue #built with install_rpath hopefully changes.append("-change '%s' '@rpath/%s'"%(path, copy_as)) changes = " ".join(changes) info = plistlib.readPlist(plist_path) latest_tag = cmd('git describe --tags --abbrev=0') log = cmd('git log --pretty=oneline {0}...HEAD'.format(latest_tag)) from os import path # set version if args.stable: info["CFBundleVersion"] = latest_tag info["CFBundleShortVersionString"] = latest_tag info["SUFeedURL"] = '{0}/stable/updates.xml'.format(args.base_url) else: info["CFBundleVersion"] = args.build_number info["CFBundleShortVersionString"] = '{0}.{1}'.format(latest_tag, args.build_number) info["SUFeedURL"] = '{0}/{1}/{2}/updates.xml'.format(args.base_url, args.user, args.channel) info["SUPublicDSAKeyFile"] = path.basename(args.public_key) info["OBSFeedsURL"] = '{0}/feeds.xml'.format(args.base_url) app_name = info["CFBundleName"]+".app" icon_file = "tmp/Contents/Resources/%s"%info["CFBundleIconFile"] copytree(build_path, "tmp/Contents/Resources/", symlinks=True) copy(icon_path, icon_file) plistlib.writePlist(info, "tmp/Contents/Info.plist") makedirs("tmp/Contents/MacOS") copy(run_path, "tmp/Contents/MacOS/%s"%info["CFBundleExecutable"]) try: copy(args.public_key, "tmp/Contents/Resources") except: pass if args.sparkle is not None: copytree(args.sparkle, "tmp/Contents/Frameworks/Sparkle.framework", symlinks=True) prefix = "tmp/Contents/Resources/" sparkle_path = '@loader_path/{0}/Frameworks/Sparkle.framework/Versions/A/Sparkle' cmd('{0}install_name_tool -change {1} {2} {3}/bin/obs'.format( args.prefix, actual_sparkle_path, sparkle_path.format('../..'), prefix)) for path, external, copy_as in inspected: id_ = "" filename = path rpath = "" if external: if copy_as == "Python": continue id_ = "-id '@rpath/%s'"%copy_as filename = prefix + "bin/" +copy_as rpath = "-add_rpath @loader_path/ -add_rpath @executable_path/" if "/" in copy_as: try: dirs = copy_as.rsplit("/", 1)[0] makedirs(prefix + "bin/" + dirs) except: pass copy(path, filename) else: filename = path[len(build_path)+1:] id_ = "-id '@rpath/../%s'"%filename if not filename.startswith("bin"): print(filename) rpath = "-add_rpath '@loader_path/{}/'".format(ospath.relpath("bin/", ospath.dirname(filename))) filename = prefix + filename cmd = "{0}install_name_tool {1} {2} {3} '{4}'".format(args.prefix, changes, id_, rpath, filename) call(cmd, shell=True) try: rename("tmp", app_name) except: print("App already exists") rmtree("tmp")