build_app.py 6.9 KB

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