build_app.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. #!/usr/bin/env python
  2. candidate_paths = "bin obs-plugins data".split()
  3. plist_path = "../cmake/osxbundle/Info.plist"
  4. icon_path = "../cmake/osxbundle/obs.icns"
  5. run_path = "../cmake/osxbundle/obslaunch.sh"
  6. #not copied
  7. blacklist = """/usr /System""".split()
  8. #copied
  9. whitelist = """/usr/local""".split()
  10. #
  11. #
  12. #
  13. from sys import argv
  14. from glob import glob
  15. from subprocess import check_output, call
  16. from collections import namedtuple
  17. from shutil import copy, copytree, rmtree
  18. from os import makedirs, rename, walk, path as ospath
  19. import plistlib
  20. import argparse
  21. def _str_to_bool(s):
  22. """Convert string to bool (in argparse context)."""
  23. if s.lower() not in ['true', 'false']:
  24. raise ValueError('Need bool; got %r' % s)
  25. return {'true': True, 'false': False}[s.lower()]
  26. def add_boolean_argument(parser, name, default=False):
  27. """Add a boolean argument to an ArgumentParser instance."""
  28. group = parser.add_mutually_exclusive_group()
  29. group.add_argument(
  30. '--' + name, nargs='?', default=default, const=True, type=_str_to_bool)
  31. group.add_argument('--no' + name, dest=name, action='store_false')
  32. parser = argparse.ArgumentParser(description='obs-studio package util')
  33. parser.add_argument('-d', '--base-dir', dest='dir', default='rundir/RelWithDebInfo')
  34. parser.add_argument('-n', '--build-number', dest='build_number', default='0')
  35. parser.add_argument('-k', '--public-key', dest='public_key', default='OBSPublicDSAKey.pem')
  36. parser.add_argument('-f', '--sparkle-framework', dest='sparkle', default=None)
  37. parser.add_argument('-b', '--base-url', dest='base_url', default='https://obsproject.com/osx_update')
  38. parser.add_argument('-u', '--user', dest='user', default='jp9000')
  39. parser.add_argument('-c', '--channel', dest='channel', default='master')
  40. add_boolean_argument(parser, 'stable', default=False)
  41. parser.add_argument('-p', '--prefix', dest='prefix', default='')
  42. args = parser.parse_args()
  43. def cmd(cmd):
  44. import subprocess
  45. import shlex
  46. return subprocess.check_output(shlex.split(cmd)).rstrip('\r\n')
  47. LibTarget = namedtuple("LibTarget", ("path", "external", "copy_as"))
  48. inspect = list()
  49. inspected = set()
  50. build_path = args.dir
  51. build_path = build_path.replace("\\ ", " ")
  52. def add(name, external=False, copy_as=None):
  53. if external and copy_as is None:
  54. copy_as = name.split("/")[-1]
  55. if name[0] != "/":
  56. name = build_path+"/"+name
  57. t = LibTarget(name, external, copy_as)
  58. if t in inspected:
  59. return
  60. inspect.append(t)
  61. inspected.add(t)
  62. for i in candidate_paths:
  63. print("Checking " + i)
  64. for root, dirs, files in walk(build_path+"/"+i):
  65. for file_ in files:
  66. if ".ini" in file_:
  67. continue
  68. if ".png" in file_:
  69. continue
  70. if ".effect" in file_:
  71. continue
  72. if ".py" in file_:
  73. continue
  74. if ".json" in file_:
  75. continue
  76. path = root + "/" + file_
  77. try:
  78. out = check_output("{0}otool -L '{1}'".format(args.prefix, path), shell=True,
  79. universal_newlines=True)
  80. if "is not an object file" in out:
  81. continue
  82. except:
  83. continue
  84. rel_path = path[len(build_path)+1:]
  85. print(repr(path), repr(rel_path))
  86. add(rel_path)
  87. def add_plugins(path, replace):
  88. for img in glob(path.replace(
  89. "lib/QtCore.framework/Versions/5/QtCore",
  90. "plugins/%s/*"%replace).replace(
  91. "Library/Frameworks/QtCore.framework/Versions/5/QtCore",
  92. "share/qt5/plugins/%s/*"%replace)):
  93. if "_debug" in img:
  94. continue
  95. add(img, True, img.split("plugins/")[-1])
  96. actual_sparkle_path = '@loader_path/Frameworks/Sparkle.framework/Versions/A/Sparkle'
  97. while inspect:
  98. target = inspect.pop()
  99. print("inspecting", repr(target))
  100. path = target.path
  101. if path[0] == "@":
  102. continue
  103. out = check_output("{0}otool -L '{1}'".format(args.prefix, path), shell=True,
  104. universal_newlines=True)
  105. if "QtCore" in path:
  106. add_plugins(path, "platforms")
  107. add_plugins(path, "imageformats")
  108. add_plugins(path, "accessible")
  109. add_plugins(path, "styles")
  110. for line in out.split("\n")[1:]:
  111. new = line.strip().split(" (")[0]
  112. if '@' in new and "sparkle.framework" in new.lower():
  113. actual_sparkle_path = new
  114. print "Using sparkle path:", repr(actual_sparkle_path)
  115. if not new or new[0] == "@" or new.endswith(path.split("/")[-1]):
  116. continue
  117. whitelisted = False
  118. for i in whitelist:
  119. if new.startswith(i):
  120. whitelisted = True
  121. if not whitelisted:
  122. blacklisted = False
  123. for i in blacklist:
  124. if new.startswith(i):
  125. blacklisted = True
  126. break
  127. if blacklisted:
  128. continue
  129. add(new, True)
  130. changes = list()
  131. for path, external, copy_as in inspected:
  132. if not external:
  133. continue #built with install_rpath hopefully
  134. changes.append("-change '%s' '@rpath/%s'"%(path, copy_as))
  135. changes = " ".join(changes)
  136. info = plistlib.readPlist(plist_path)
  137. latest_tag = cmd('git describe --tags --abbrev=0')
  138. log = cmd('git log --pretty=oneline {0}...HEAD'.format(latest_tag))
  139. from os import path
  140. # set version
  141. if args.stable:
  142. info["CFBundleVersion"] = latest_tag
  143. info["CFBundleShortVersionString"] = latest_tag
  144. info["SUFeedURL"] = '{0}/stable/updates.xml'.format(args.base_url)
  145. else:
  146. info["CFBundleVersion"] = args.build_number
  147. info["CFBundleShortVersionString"] = '{0}.{1}'.format(latest_tag, args.build_number)
  148. info["SUFeedURL"] = '{0}/{1}/{2}/updates.xml'.format(args.base_url, args.user, args.channel)
  149. info["SUPublicDSAKeyFile"] = path.basename(args.public_key)
  150. info["OBSFeedsURL"] = '{0}/feeds.xml'.format(args.base_url)
  151. app_name = info["CFBundleName"]+".app"
  152. icon_file = "tmp/Contents/Resources/%s"%info["CFBundleIconFile"]
  153. copytree(build_path, "tmp/Contents/Resources/", symlinks=True)
  154. copy(icon_path, icon_file)
  155. plistlib.writePlist(info, "tmp/Contents/Info.plist")
  156. makedirs("tmp/Contents/MacOS")
  157. copy(run_path, "tmp/Contents/MacOS/%s"%info["CFBundleExecutable"])
  158. try:
  159. copy(args.public_key, "tmp/Contents/Resources")
  160. except:
  161. pass
  162. if args.sparkle is not None:
  163. copytree(args.sparkle, "tmp/Contents/Frameworks/Sparkle.framework", symlinks=True)
  164. prefix = "tmp/Contents/Resources/"
  165. sparkle_path = '@loader_path/{0}/Frameworks/Sparkle.framework/Versions/A/Sparkle'
  166. cmd('{0}install_name_tool -change {1} {2} {3}/bin/obs'.format(
  167. args.prefix, actual_sparkle_path, sparkle_path.format('../..'), prefix))
  168. for path, external, copy_as in inspected:
  169. id_ = ""
  170. filename = path
  171. rpath = ""
  172. if external:
  173. if copy_as == "Python":
  174. continue
  175. id_ = "-id '@rpath/%s'"%copy_as
  176. filename = prefix + "bin/" +copy_as
  177. rpath = "-add_rpath @loader_path/ -add_rpath @executable_path/"
  178. if "/" in copy_as:
  179. try:
  180. dirs = copy_as.rsplit("/", 1)[0]
  181. makedirs(prefix + "bin/" + dirs)
  182. except:
  183. pass
  184. copy(path, filename)
  185. else:
  186. filename = path[len(build_path)+1:]
  187. id_ = "-id '@rpath/../%s'"%filename
  188. if not filename.startswith("bin"):
  189. print(filename)
  190. rpath = "-add_rpath '@loader_path/{}/'".format(ospath.relpath("bin/", ospath.dirname(filename)))
  191. filename = prefix + filename
  192. cmd = "{0}install_name_tool {1} {2} {3} '{4}'".format(args.prefix, changes, id_, rpath, filename)
  193. call(cmd, shell=True)
  194. try:
  195. rename("tmp", app_name)
  196. except:
  197. print("App already exists")
  198. rmtree("tmp")