build_app.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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://builds.catchexception.org/obs-studio')
  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. path = root + "/" + file_
  67. try:
  68. out = check_output("{0}otool -L '{1}'".format(args.prefix, path), shell=True,
  69. universal_newlines=True)
  70. if "is not an object file" in out:
  71. continue
  72. except:
  73. continue
  74. rel_path = path[len(build_path)+1:]
  75. print(repr(path), repr(rel_path))
  76. add(rel_path)
  77. def add_plugins(path, replace):
  78. for img in glob(path.replace(
  79. "lib/QtCore.framework/Versions/5/QtCore",
  80. "plugins/%s/*"%replace).replace(
  81. "Library/Frameworks/QtCore.framework/Versions/5/QtCore",
  82. "share/qt5/plugins/%s/*"%replace)):
  83. if "_debug" in img:
  84. continue
  85. add(img, True, img.split("plugins/")[-1])
  86. actual_sparkle_path = '@loader_path/Frameworks/Sparkle.framework/Versions/A/Sparkle'
  87. while inspect:
  88. target = inspect.pop()
  89. print("inspecting", repr(target))
  90. path = target.path
  91. if path[0] == "@":
  92. continue
  93. out = check_output("{0}otool -L '{1}'".format(args.prefix, path), shell=True,
  94. universal_newlines=True)
  95. if "QtCore" in path:
  96. add_plugins(path, "platforms")
  97. add_plugins(path, "imageformats")
  98. add_plugins(path, "accessible")
  99. for line in out.split("\n")[1:]:
  100. new = line.strip().split(" (")[0]
  101. if '@' in new and "sparkle.framework" in new.lower():
  102. actual_sparkle_path = new
  103. print "Using sparkle path:", repr(actual_sparkle_path)
  104. if not new or new[0] == "@" or new.endswith(path.split("/")[-1]):
  105. continue
  106. whitelisted = False
  107. for i in whitelist:
  108. if new.startswith(i):
  109. whitelisted = True
  110. if not whitelisted:
  111. blacklisted = False
  112. for i in blacklist:
  113. if new.startswith(i):
  114. blacklisted = True
  115. break
  116. if blacklisted:
  117. continue
  118. add(new, True)
  119. changes = list()
  120. for path, external, copy_as in inspected:
  121. if not external:
  122. continue #built with install_rpath hopefully
  123. changes.append("-change '%s' '@rpath/%s'"%(path, copy_as))
  124. changes = " ".join(changes)
  125. info = plistlib.readPlist(plist_path)
  126. latest_tag = cmd('git describe --tags --abbrev=0')
  127. log = cmd('git log --pretty=oneline {0}...HEAD'.format(latest_tag))
  128. from os import path
  129. # set version
  130. if args.stable:
  131. info["CFBundleVersion"] = latest_tag
  132. info["CFBundleShortVersionString"] = latest_tag
  133. info["SUFeedURL"] = '{0}/stable/updates.xml'.format(args.base_url)
  134. else:
  135. info["CFBundleVersion"] = args.build_number
  136. info["CFBundleShortVersionString"] = '{0}.{1}'.format(latest_tag, args.build_number)
  137. info["SUFeedURL"] = '{0}/{1}/{2}/updates.xml'.format(args.base_url, args.user, args.channel)
  138. info["SUPublicDSAKeyFile"] = path.basename(args.public_key)
  139. info["OBSFeedsURL"] = '{0}/feeds.xml'.format(args.base_url)
  140. app_name = info["CFBundleName"]+".app"
  141. icon_file = "tmp/Contents/Resources/%s"%info["CFBundleIconFile"]
  142. copytree(build_path, "tmp/Contents/Resources/", symlinks=True)
  143. copy(icon_path, icon_file)
  144. plistlib.writePlist(info, "tmp/Contents/Info.plist")
  145. makedirs("tmp/Contents/MacOS")
  146. copy(run_path, "tmp/Contents/MacOS/%s"%info["CFBundleExecutable"])
  147. try:
  148. copy(args.public_key, "tmp/Contents/Resources")
  149. except:
  150. pass
  151. if args.sparkle is not None:
  152. copytree(args.sparkle, "tmp/Contents/Frameworks/Sparkle.framework", symlinks=True)
  153. prefix = "tmp/Contents/Resources/"
  154. sparkle_path = '@loader_path/{0}/Frameworks/Sparkle.framework/Versions/A/Sparkle'
  155. cmd('{0}install_name_tool -change {1} {2} {3}/bin/obs'.format(
  156. args.prefix, actual_sparkle_path, sparkle_path.format('../..'), prefix))
  157. for path, external, copy_as in inspected:
  158. id_ = ""
  159. filename = path
  160. rpath = ""
  161. if external:
  162. id_ = "-id '@rpath/%s'"%copy_as
  163. filename = prefix + "bin/" +copy_as
  164. rpath = "-add_rpath @loader_path/ -add_rpath @executable_path/"
  165. if "/" in copy_as:
  166. try:
  167. dirs = copy_as.rsplit("/", 1)[0]
  168. makedirs(prefix + "bin/" + dirs)
  169. except:
  170. pass
  171. copy(path, filename)
  172. else:
  173. filename = path[len(build_path)+1:]
  174. id_ = "-id '@rpath/../%s'"%filename
  175. if not filename.startswith("bin"):
  176. print(filename)
  177. rpath = "-add_rpath '@loader_path/{}/'".format(ospath.relpath("bin/", ospath.dirname(filename)))
  178. filename = prefix + filename
  179. cmd = "{0}install_name_tool {1} {2} {3} '{4}'".format(args.prefix, changes, id_, rpath, filename)
  180. call(cmd, shell=True)
  181. try:
  182. rename("tmp", app_name)
  183. except:
  184. print("App already exists")
  185. rmtree("tmp")