Browse Source

Merge pull request #660 from DDRBoxman/buildapp

CI: Package OSX build into an actual app
Jim 9 years ago
parent
commit
925c229312
3 changed files with 221 additions and 4 deletions
  1. 4 3
      CI/before-deploy-osx.sh
  2. 1 1
      CI/install-dependencies-osx.sh
  3. 216 0
      CI/install/osx/build_app.py

+ 4 - 3
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

+ 1 - 1
CI/install-dependencies-osx.sh

@@ -1,2 +1,2 @@
 brew update
-brew install ffmpeg x264 qt5
+brew install ffmpeg x264 qt5 python3

+ 216 - 0
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")