diff --git a/CI/before-deploy-osx.sh b/CI/before-deploy-osx.sh index 684ae7760..0ccb931f9 100755 --- a/CI/before-deploy-osx.sh +++ b/CI/before-deploy-osx.sh @@ -2,6 +2,7 @@ export GIT_HASH=$(git rev-parse --short HEAD) export FILE_DATE=$(date +%Y-%m-%d.%H:%M:%S) export FILENAME=$FILE_DATE-$GIT_HASH-osx.zip mkdir nightly -cd ./build/rundir/RelWithDebInfo -zip -r -X $FILENAME . -mv ./$FILENAME ../../../nightly +cd ./build +sudo python3 ../CI/install/osx/build_app.py +zip -r -X $FILENAME OBS.app +mv ./$FILENAME ../nightly diff --git a/CI/install-dependencies-osx.sh b/CI/install-dependencies-osx.sh index 648d4d196..739132711 100755 --- a/CI/install-dependencies-osx.sh +++ b/CI/install-dependencies-osx.sh @@ -1,2 +1,2 @@ brew update -brew install ffmpeg x264 qt5 \ No newline at end of file +brew install ffmpeg x264 qt5 python3 \ No newline at end of file diff --git a/CI/install/osx/build_app.py b/CI/install/osx/build_app.py new file mode 100644 index 000000000..f47a47428 --- /dev/null +++ b/CI/install/osx/build_app.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3.3 + +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 +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://builds.catchexception.org/obs-studio') +parser.add_argument('-u', '--user', dest='user', default='jp9000') +parser.add_argument('-c', '--channel', dest='channel', default='master') +parser.add_argument('-s', '--stable', dest='stable', required=False, action='store_true', 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: + 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: + pass + 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 + 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: + pass + + if "QtCore" in path: + add_plugins(path, "platforms") + add_plugins(path, "imageformats") + add_plugins(path, "accessible") + + + 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="TACO" #= cmd('git describe --tags --abbrev=0') +log ="BUTTS"#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: + 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")