You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

macdeployqtplus 36KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891
  1. #!/usr/bin/env python
  2. from __future__ import division, print_function, unicode_literals
  3. #
  4. # Copyright (C) 2011 Patrick "p2k" Schneider <me@p2k-network.org>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. #
  19. import subprocess, sys, re, os, shutil, stat, os.path, time
  20. from string import Template
  21. from argparse import ArgumentParser
  22. # This is ported from the original macdeployqt with modifications
  23. class FrameworkInfo(object):
  24. def __init__(self):
  25. self.frameworkDirectory = ""
  26. self.frameworkName = ""
  27. self.frameworkPath = ""
  28. self.binaryDirectory = ""
  29. self.binaryName = ""
  30. self.binaryPath = ""
  31. self.version = ""
  32. self.installName = ""
  33. self.deployedInstallName = ""
  34. self.sourceFilePath = ""
  35. self.destinationDirectory = ""
  36. self.sourceResourcesDirectory = ""
  37. self.sourceVersionContentsDirectory = ""
  38. self.sourceContentsDirectory = ""
  39. self.destinationResourcesDirectory = ""
  40. self.destinationVersionContentsDirectory = ""
  41. def __eq__(self, other):
  42. if self.__class__ == other.__class__:
  43. return self.__dict__ == other.__dict__
  44. else:
  45. return False
  46. def __str__(self):
  47. return """ Framework name: %s
  48. Framework directory: %s
  49. Framework path: %s
  50. Binary name: %s
  51. Binary directory: %s
  52. Binary path: %s
  53. Version: %s
  54. Install name: %s
  55. Deployed install name: %s
  56. Source file Path: %s
  57. Deployed Directory (relative to bundle): %s
  58. """ % (self.frameworkName,
  59. self.frameworkDirectory,
  60. self.frameworkPath,
  61. self.binaryName,
  62. self.binaryDirectory,
  63. self.binaryPath,
  64. self.version,
  65. self.installName,
  66. self.deployedInstallName,
  67. self.sourceFilePath,
  68. self.destinationDirectory)
  69. def isDylib(self):
  70. return self.frameworkName.endswith(".dylib")
  71. def isQtFramework(self):
  72. if self.isDylib():
  73. return self.frameworkName.startswith("libQt")
  74. else:
  75. return self.frameworkName.startswith("Qt")
  76. reOLine = re.compile(r'^(.+) \(compatibility version [0-9.]+, current version [0-9.]+\)$')
  77. bundleFrameworkDirectory = "Contents/Frameworks"
  78. bundleBinaryDirectory = "Contents/MacOS"
  79. @classmethod
  80. def fromOtoolLibraryLine(cls, line):
  81. # Note: line must be trimmed
  82. if line == "":
  83. return None
  84. # Don't deploy system libraries (exception for libQtuitools and libQtlucene).
  85. if line.startswith("/System/Library/") or line.startswith("@executable_path") or (line.startswith("/usr/lib/") and "libQt" not in line):
  86. return None
  87. m = cls.reOLine.match(line)
  88. if m is None:
  89. raise RuntimeError("otool line could not be parsed: " + line)
  90. path = m.group(1)
  91. info = cls()
  92. info.sourceFilePath = path
  93. info.installName = path
  94. if path.endswith(".dylib"):
  95. dirname, filename = os.path.split(path)
  96. info.frameworkName = filename
  97. info.frameworkDirectory = dirname
  98. info.frameworkPath = path
  99. info.binaryDirectory = dirname
  100. info.binaryName = filename
  101. info.binaryPath = path
  102. info.version = "-"
  103. info.installName = path
  104. info.deployedInstallName = "@executable_path/../Frameworks/" + info.binaryName
  105. info.sourceFilePath = path
  106. info.destinationDirectory = cls.bundleFrameworkDirectory
  107. else:
  108. parts = path.split("/")
  109. i = 0
  110. # Search for the .framework directory
  111. for part in parts:
  112. if part.endswith(".framework"):
  113. break
  114. i += 1
  115. if i == len(parts):
  116. raise RuntimeError("Could not find .framework or .dylib in otool line: " + line)
  117. info.frameworkName = parts[i]
  118. info.frameworkDirectory = "/".join(parts[:i])
  119. info.frameworkPath = os.path.join(info.frameworkDirectory, info.frameworkName)
  120. info.binaryName = parts[i+3]
  121. info.binaryDirectory = "/".join(parts[i+1:i+3])
  122. info.binaryPath = os.path.join(info.binaryDirectory, info.binaryName)
  123. info.version = parts[i+2]
  124. info.deployedInstallName = "@executable_path/../Frameworks/" + os.path.join(info.frameworkName, info.binaryPath)
  125. info.destinationDirectory = os.path.join(cls.bundleFrameworkDirectory, info.frameworkName, info.binaryDirectory)
  126. info.sourceResourcesDirectory = os.path.join(info.frameworkPath, "Resources")
  127. info.sourceContentsDirectory = os.path.join(info.frameworkPath, "Contents")
  128. info.sourceVersionContentsDirectory = os.path.join(info.frameworkPath, "Versions", info.version, "Contents")
  129. info.destinationResourcesDirectory = os.path.join(cls.bundleFrameworkDirectory, info.frameworkName, "Resources")
  130. info.destinationContentsDirectory = os.path.join(cls.bundleFrameworkDirectory, info.frameworkName, "Contents")
  131. info.destinationVersionContentsDirectory = os.path.join(cls.bundleFrameworkDirectory, info.frameworkName, "Versions", info.version, "Contents")
  132. return info
  133. class ApplicationBundleInfo(object):
  134. def __init__(self, path):
  135. self.path = path
  136. appName = "Bitcoin-Qt"
  137. self.binaryPath = os.path.join(path, "Contents", "MacOS", appName)
  138. if not os.path.exists(self.binaryPath):
  139. raise RuntimeError("Could not find bundle binary for " + path)
  140. self.resourcesPath = os.path.join(path, "Contents", "Resources")
  141. self.pluginPath = os.path.join(path, "Contents", "PlugIns")
  142. class DeploymentInfo(object):
  143. def __init__(self):
  144. self.qtPath = None
  145. self.pluginPath = None
  146. self.deployedFrameworks = []
  147. def detectQtPath(self, frameworkDirectory):
  148. parentDir = os.path.dirname(frameworkDirectory)
  149. if os.path.exists(os.path.join(parentDir, "translations")):
  150. # Classic layout, e.g. "/usr/local/Trolltech/Qt-4.x.x"
  151. self.qtPath = parentDir
  152. elif os.path.exists(os.path.join(parentDir, "share", "qt4", "translations")):
  153. # MacPorts layout, e.g. "/opt/local/share/qt4"
  154. self.qtPath = os.path.join(parentDir, "share", "qt4")
  155. elif os.path.exists(os.path.join(os.path.dirname(parentDir), "share", "qt4", "translations")):
  156. # Newer Macports layout
  157. self.qtPath = os.path.join(os.path.dirname(parentDir), "share", "qt4")
  158. else:
  159. self.qtPath = os.getenv("QTDIR", None)
  160. if self.qtPath is not None:
  161. pluginPath = os.path.join(self.qtPath, "plugins")
  162. if os.path.exists(pluginPath):
  163. self.pluginPath = pluginPath
  164. def usesFramework(self, name):
  165. nameDot = "%s." % name
  166. libNameDot = "lib%s." % name
  167. for framework in self.deployedFrameworks:
  168. if framework.endswith(".framework"):
  169. if framework.startswith(nameDot):
  170. return True
  171. elif framework.endswith(".dylib"):
  172. if framework.startswith(libNameDot):
  173. return True
  174. return False
  175. def getFrameworks(binaryPath, verbose):
  176. if verbose >= 3:
  177. print("Inspecting with otool: " + binaryPath)
  178. otoolbin=os.getenv("OTOOL", "otool")
  179. otool = subprocess.Popen([otoolbin, "-L", binaryPath], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  180. o_stdout, o_stderr = otool.communicate()
  181. if otool.returncode != 0:
  182. if verbose >= 1:
  183. sys.stderr.write(o_stderr)
  184. sys.stderr.flush()
  185. raise RuntimeError("otool failed with return code %d" % otool.returncode)
  186. otoolLines = o_stdout.decode().split("\n")
  187. otoolLines.pop(0) # First line is the inspected binary
  188. if ".framework" in binaryPath or binaryPath.endswith(".dylib"):
  189. otoolLines.pop(0) # Frameworks and dylibs list themselves as a dependency.
  190. libraries = []
  191. for line in otoolLines:
  192. line = line.replace("@loader_path", os.path.dirname(binaryPath))
  193. info = FrameworkInfo.fromOtoolLibraryLine(line.strip())
  194. if info is not None:
  195. if verbose >= 3:
  196. print("Found framework:")
  197. print(info)
  198. libraries.append(info)
  199. return libraries
  200. def runInstallNameTool(action, *args):
  201. installnametoolbin=os.getenv("INSTALLNAMETOOL", "install_name_tool")
  202. subprocess.check_call([installnametoolbin, "-"+action] + list(args))
  203. def changeInstallName(oldName, newName, binaryPath, verbose):
  204. if verbose >= 3:
  205. print("Using install_name_tool:")
  206. print(" in", binaryPath)
  207. print(" change reference", oldName)
  208. print(" to", newName)
  209. runInstallNameTool("change", oldName, newName, binaryPath)
  210. def changeIdentification(id, binaryPath, verbose):
  211. if verbose >= 3:
  212. print("Using install_name_tool:")
  213. print(" change identification in", binaryPath)
  214. print(" to", id)
  215. runInstallNameTool("id", id, binaryPath)
  216. def runStrip(binaryPath, verbose):
  217. stripbin=os.getenv("STRIP", "strip")
  218. if verbose >= 3:
  219. print("Using strip:")
  220. print(" stripped", binaryPath)
  221. subprocess.check_call([stripbin, "-x", binaryPath])
  222. def copyFramework(framework, path, verbose):
  223. if framework.sourceFilePath.startswith("Qt"):
  224. #standard place for Nokia Qt installer's frameworks
  225. fromPath = "/Library/Frameworks/" + framework.sourceFilePath
  226. else:
  227. fromPath = framework.sourceFilePath
  228. toDir = os.path.join(path, framework.destinationDirectory)
  229. toPath = os.path.join(toDir, framework.binaryName)
  230. if not os.path.exists(fromPath):
  231. raise RuntimeError("No file at " + fromPath)
  232. if os.path.exists(toPath):
  233. return None # Already there
  234. if not os.path.exists(toDir):
  235. os.makedirs(toDir)
  236. shutil.copy2(fromPath, toPath)
  237. if verbose >= 3:
  238. print("Copied:", fromPath)
  239. print(" to:", toPath)
  240. permissions = os.stat(toPath)
  241. if not permissions.st_mode & stat.S_IWRITE:
  242. os.chmod(toPath, permissions.st_mode | stat.S_IWRITE)
  243. if not framework.isDylib(): # Copy resources for real frameworks
  244. linkfrom = os.path.join(path, "Contents","Frameworks", framework.frameworkName, "Versions", "Current")
  245. linkto = framework.version
  246. if not os.path.exists(linkfrom):
  247. os.symlink(linkto, linkfrom)
  248. if verbose >= 2:
  249. print("Linked:", linkfrom, "->", linkto)
  250. fromResourcesDir = framework.sourceResourcesDirectory
  251. if os.path.exists(fromResourcesDir):
  252. toResourcesDir = os.path.join(path, framework.destinationResourcesDirectory)
  253. shutil.copytree(fromResourcesDir, toResourcesDir, symlinks=True)
  254. if verbose >= 3:
  255. print("Copied resources:", fromResourcesDir)
  256. print(" to:", toResourcesDir)
  257. fromContentsDir = framework.sourceVersionContentsDirectory
  258. if not os.path.exists(fromContentsDir):
  259. fromContentsDir = framework.sourceContentsDirectory
  260. if os.path.exists(fromContentsDir):
  261. toContentsDir = os.path.join(path, framework.destinationVersionContentsDirectory)
  262. shutil.copytree(fromContentsDir, toContentsDir, symlinks=True)
  263. contentslinkfrom = os.path.join(path, framework.destinationContentsDirectory)
  264. if verbose >= 3:
  265. print("Copied Contents:", fromContentsDir)
  266. print(" to:", toContentsDir)
  267. elif framework.frameworkName.startswith("libQtGui"): # Copy qt_menu.nib (applies to non-framework layout)
  268. qtMenuNibSourcePath = os.path.join(framework.frameworkDirectory, "Resources", "qt_menu.nib")
  269. qtMenuNibDestinationPath = os.path.join(path, "Contents", "Resources", "qt_menu.nib")
  270. if os.path.exists(qtMenuNibSourcePath) and not os.path.exists(qtMenuNibDestinationPath):
  271. shutil.copytree(qtMenuNibSourcePath, qtMenuNibDestinationPath, symlinks=True)
  272. if verbose >= 3:
  273. print("Copied for libQtGui:", qtMenuNibSourcePath)
  274. print(" to:", qtMenuNibDestinationPath)
  275. return toPath
  276. def deployFrameworks(frameworks, bundlePath, binaryPath, strip, verbose, deploymentInfo=None):
  277. if deploymentInfo is None:
  278. deploymentInfo = DeploymentInfo()
  279. while len(frameworks) > 0:
  280. framework = frameworks.pop(0)
  281. deploymentInfo.deployedFrameworks.append(framework.frameworkName)
  282. if verbose >= 2:
  283. print("Processing", framework.frameworkName, "...")
  284. # Get the Qt path from one of the Qt frameworks
  285. if deploymentInfo.qtPath is None and framework.isQtFramework():
  286. deploymentInfo.detectQtPath(framework.frameworkDirectory)
  287. if framework.installName.startswith("@executable_path") or framework.installName.startswith(bundlePath):
  288. if verbose >= 2:
  289. print(framework.frameworkName, "already deployed, skipping.")
  290. continue
  291. # install_name_tool the new id into the binary
  292. changeInstallName(framework.installName, framework.deployedInstallName, binaryPath, verbose)
  293. # Copy farmework to app bundle.
  294. deployedBinaryPath = copyFramework(framework, bundlePath, verbose)
  295. # Skip the rest if already was deployed.
  296. if deployedBinaryPath is None:
  297. continue
  298. if strip:
  299. runStrip(deployedBinaryPath, verbose)
  300. # install_name_tool it a new id.
  301. changeIdentification(framework.deployedInstallName, deployedBinaryPath, verbose)
  302. # Check for framework dependencies
  303. dependencies = getFrameworks(deployedBinaryPath, verbose)
  304. for dependency in dependencies:
  305. changeInstallName(dependency.installName, dependency.deployedInstallName, deployedBinaryPath, verbose)
  306. # Deploy framework if necessary.
  307. if dependency.frameworkName not in deploymentInfo.deployedFrameworks and dependency not in frameworks:
  308. frameworks.append(dependency)
  309. return deploymentInfo
  310. def deployFrameworksForAppBundle(applicationBundle, strip, verbose):
  311. frameworks = getFrameworks(applicationBundle.binaryPath, verbose)
  312. if len(frameworks) == 0 and verbose >= 1:
  313. print("Warning: Could not find any external frameworks to deploy in %s." % (applicationBundle.path))
  314. return DeploymentInfo()
  315. else:
  316. return deployFrameworks(frameworks, applicationBundle.path, applicationBundle.binaryPath, strip, verbose)
  317. def deployPlugins(appBundleInfo, deploymentInfo, strip, verbose):
  318. # Lookup available plugins, exclude unneeded
  319. plugins = []
  320. if deploymentInfo.pluginPath is None:
  321. return
  322. for dirpath, dirnames, filenames in os.walk(deploymentInfo.pluginPath):
  323. pluginDirectory = os.path.relpath(dirpath, deploymentInfo.pluginPath)
  324. if pluginDirectory == "designer":
  325. # Skip designer plugins
  326. continue
  327. elif pluginDirectory == "phonon" or pluginDirectory == "phonon_backend":
  328. # Deploy the phonon plugins only if phonon is in use
  329. if not deploymentInfo.usesFramework("phonon"):
  330. continue
  331. elif pluginDirectory == "sqldrivers":
  332. # Deploy the sql plugins only if QtSql is in use
  333. if not deploymentInfo.usesFramework("QtSql"):
  334. continue
  335. elif pluginDirectory == "script":
  336. # Deploy the script plugins only if QtScript is in use
  337. if not deploymentInfo.usesFramework("QtScript"):
  338. continue
  339. elif pluginDirectory == "qmltooling" or pluginDirectory == "qml1tooling":
  340. # Deploy the qml plugins only if QtDeclarative is in use
  341. if not deploymentInfo.usesFramework("QtDeclarative"):
  342. continue
  343. elif pluginDirectory == "bearer":
  344. # Deploy the bearer plugins only if QtNetwork is in use
  345. if not deploymentInfo.usesFramework("QtNetwork"):
  346. continue
  347. elif pluginDirectory == "position":
  348. # Deploy the position plugins only if QtPositioning is in use
  349. if not deploymentInfo.usesFramework("QtPositioning"):
  350. continue
  351. elif pluginDirectory == "sensors" or pluginDirectory == "sensorgestures":
  352. # Deploy the sensor plugins only if QtSensors is in use
  353. if not deploymentInfo.usesFramework("QtSensors"):
  354. continue
  355. elif pluginDirectory == "audio" or pluginDirectory == "playlistformats":
  356. # Deploy the audio plugins only if QtMultimedia is in use
  357. if not deploymentInfo.usesFramework("QtMultimedia"):
  358. continue
  359. elif pluginDirectory == "mediaservice":
  360. # Deploy the mediaservice plugins only if QtMultimediaWidgets is in use
  361. if not deploymentInfo.usesFramework("QtMultimediaWidgets"):
  362. continue
  363. for pluginName in filenames:
  364. pluginPath = os.path.join(pluginDirectory, pluginName)
  365. if pluginName.endswith("_debug.dylib"):
  366. # Skip debug plugins
  367. continue
  368. elif pluginPath == "imageformats/libqsvg.dylib" or pluginPath == "iconengines/libqsvgicon.dylib":
  369. # Deploy the svg plugins only if QtSvg is in use
  370. if not deploymentInfo.usesFramework("QtSvg"):
  371. continue
  372. elif pluginPath == "accessible/libqtaccessiblecompatwidgets.dylib":
  373. # Deploy accessibility for Qt3Support only if the Qt3Support is in use
  374. if not deploymentInfo.usesFramework("Qt3Support"):
  375. continue
  376. elif pluginPath == "graphicssystems/libqglgraphicssystem.dylib":
  377. # Deploy the opengl graphicssystem plugin only if QtOpenGL is in use
  378. if not deploymentInfo.usesFramework("QtOpenGL"):
  379. continue
  380. elif pluginPath == "accessible/libqtaccessiblequick.dylib":
  381. # Deploy the accessible qtquick plugin only if QtQuick is in use
  382. if not deploymentInfo.usesFramework("QtQuick"):
  383. continue
  384. plugins.append((pluginDirectory, pluginName))
  385. for pluginDirectory, pluginName in plugins:
  386. if verbose >= 2:
  387. print("Processing plugin", os.path.join(pluginDirectory, pluginName), "...")
  388. sourcePath = os.path.join(deploymentInfo.pluginPath, pluginDirectory, pluginName)
  389. destinationDirectory = os.path.join(appBundleInfo.pluginPath, pluginDirectory)
  390. if not os.path.exists(destinationDirectory):
  391. os.makedirs(destinationDirectory)
  392. destinationPath = os.path.join(destinationDirectory, pluginName)
  393. shutil.copy2(sourcePath, destinationPath)
  394. if verbose >= 3:
  395. print("Copied:", sourcePath)
  396. print(" to:", destinationPath)
  397. if strip:
  398. runStrip(destinationPath, verbose)
  399. dependencies = getFrameworks(destinationPath, verbose)
  400. for dependency in dependencies:
  401. changeInstallName(dependency.installName, dependency.deployedInstallName, destinationPath, verbose)
  402. # Deploy framework if necessary.
  403. if dependency.frameworkName not in deploymentInfo.deployedFrameworks:
  404. deployFrameworks([dependency], appBundleInfo.path, destinationPath, strip, verbose, deploymentInfo)
  405. qt_conf="""[Paths]
  406. Translations=Resources
  407. Plugins=PlugIns
  408. """
  409. ap = ArgumentParser(description="""Improved version of macdeployqt.
  410. Outputs a ready-to-deploy app in a folder "dist" and optionally wraps it in a .dmg file.
  411. Note, that the "dist" folder will be deleted before deploying on each run.
  412. Optionally, Qt translation files (.qm) and additional resources can be added to the bundle.
  413. Also optionally signs the .app bundle; set the CODESIGNARGS environment variable to pass arguments
  414. to the codesign tool.
  415. E.g. CODESIGNARGS='--sign "Developer ID Application: ..." --keychain /encrypted/foo.keychain'""")
  416. ap.add_argument("app_bundle", nargs=1, metavar="app-bundle", help="application bundle to be deployed")
  417. ap.add_argument("-verbose", type=int, nargs=1, default=[1], metavar="<0-3>", help="0 = no output, 1 = error/warning (default), 2 = normal, 3 = debug")
  418. ap.add_argument("-no-plugins", dest="plugins", action="store_false", default=True, help="skip plugin deployment")
  419. ap.add_argument("-no-strip", dest="strip", action="store_false", default=True, help="don't run 'strip' on the binaries")
  420. ap.add_argument("-sign", dest="sign", action="store_true", default=False, help="sign .app bundle with codesign tool")
  421. ap.add_argument("-dmg", nargs="?", const="", metavar="basename", help="create a .dmg disk image; if basename is not specified, a camel-cased version of the app name is used")
  422. ap.add_argument("-fancy", nargs=1, metavar="plist", default=[], help="make a fancy looking disk image using the given plist file with instructions; requires -dmg to work")
  423. ap.add_argument("-add-qt-tr", nargs=1, metavar="languages", default=[], help="add Qt translation files to the bundle's ressources; the language list must be separated with commas, not with whitespace")
  424. ap.add_argument("-translations-dir", nargs=1, metavar="path", default=None, help="Path to Qt's translation files")
  425. ap.add_argument("-add-resources", nargs="+", metavar="path", default=[], help="list of additional files or folders to be copied into the bundle's resources; must be the last argument")
  426. ap.add_argument("-volname", nargs=1, metavar="volname", default=[], help="custom volume name for dmg")
  427. config = ap.parse_args()
  428. verbose = config.verbose[0]
  429. # ------------------------------------------------
  430. app_bundle = config.app_bundle[0]
  431. if not os.path.exists(app_bundle):
  432. if verbose >= 1:
  433. sys.stderr.write("Error: Could not find app bundle \"%s\"\n" % (app_bundle))
  434. sys.exit(1)
  435. app_bundle_name = os.path.splitext(os.path.basename(app_bundle))[0]
  436. # ------------------------------------------------
  437. translations_dir = None
  438. if config.translations_dir and config.translations_dir[0]:
  439. if os.path.exists(config.translations_dir[0]):
  440. translations_dir = config.translations_dir[0]
  441. else:
  442. if verbose >= 1:
  443. sys.stderr.write("Error: Could not find translation dir \"%s\"\n" % (translations_dir))
  444. sys.exit(1)
  445. # ------------------------------------------------
  446. for p in config.add_resources:
  447. if verbose >= 3:
  448. print("Checking for \"%s\"..." % p)
  449. if not os.path.exists(p):
  450. if verbose >= 1:
  451. sys.stderr.write("Error: Could not find additional resource file \"%s\"\n" % (p))
  452. sys.exit(1)
  453. # ------------------------------------------------
  454. if len(config.fancy) == 1:
  455. if verbose >= 3:
  456. print("Fancy: Importing plistlib...")
  457. try:
  458. import plistlib
  459. except ImportError:
  460. if verbose >= 1:
  461. sys.stderr.write("Error: Could not import plistlib which is required for fancy disk images.\n")
  462. sys.exit(1)
  463. p = config.fancy[0]
  464. if verbose >= 3:
  465. print("Fancy: Loading \"%s\"..." % p)
  466. if not os.path.exists(p):
  467. if verbose >= 1:
  468. sys.stderr.write("Error: Could not find fancy disk image plist at \"%s\"\n" % (p))
  469. sys.exit(1)
  470. try:
  471. fancy = plistlib.readPlist(p)
  472. except:
  473. if verbose >= 1:
  474. sys.stderr.write("Error: Could not parse fancy disk image plist at \"%s\"\n" % (p))
  475. sys.exit(1)
  476. try:
  477. assert "window_bounds" not in fancy or (isinstance(fancy["window_bounds"], list) and len(fancy["window_bounds"]) == 4)
  478. assert "background_picture" not in fancy or isinstance(fancy["background_picture"], str)
  479. assert "icon_size" not in fancy or isinstance(fancy["icon_size"], int)
  480. assert "applications_symlink" not in fancy or isinstance(fancy["applications_symlink"], bool)
  481. if "items_position" in fancy:
  482. assert isinstance(fancy["items_position"], dict)
  483. for key, value in fancy["items_position"].items():
  484. assert isinstance(value, list) and len(value) == 2 and isinstance(value[0], int) and isinstance(value[1], int)
  485. except:
  486. if verbose >= 1:
  487. sys.stderr.write("Error: Bad format of fancy disk image plist at \"%s\"\n" % (p))
  488. sys.exit(1)
  489. if "background_picture" in fancy:
  490. bp = fancy["background_picture"]
  491. if verbose >= 3:
  492. print("Fancy: Resolving background picture \"%s\"..." % bp)
  493. if not os.path.exists(bp):
  494. bp = os.path.join(os.path.dirname(p), bp)
  495. if not os.path.exists(bp):
  496. if verbose >= 1:
  497. sys.stderr.write("Error: Could not find background picture at \"%s\" or \"%s\"\n" % (fancy["background_picture"], bp))
  498. sys.exit(1)
  499. else:
  500. fancy["background_picture"] = bp
  501. else:
  502. fancy = None
  503. # ------------------------------------------------
  504. if os.path.exists("dist"):
  505. if verbose >= 2:
  506. print("+ Removing old dist folder +")
  507. shutil.rmtree("dist")
  508. # ------------------------------------------------
  509. if len(config.volname) == 1:
  510. volname = config.volname[0]
  511. else:
  512. volname = app_bundle_name
  513. # ------------------------------------------------
  514. target = os.path.join("dist", "Bitcoin-Qt.app")
  515. if verbose >= 2:
  516. print("+ Copying source bundle +")
  517. if verbose >= 3:
  518. print(app_bundle, "->", target)
  519. os.mkdir("dist")
  520. shutil.copytree(app_bundle, target, symlinks=True)
  521. applicationBundle = ApplicationBundleInfo(target)
  522. # ------------------------------------------------
  523. if verbose >= 2:
  524. print("+ Deploying frameworks +")
  525. try:
  526. deploymentInfo = deployFrameworksForAppBundle(applicationBundle, config.strip, verbose)
  527. if deploymentInfo.qtPath is None:
  528. deploymentInfo.qtPath = os.getenv("QTDIR", None)
  529. if deploymentInfo.qtPath is None:
  530. if verbose >= 1:
  531. sys.stderr.write("Warning: Could not detect Qt's path, skipping plugin deployment!\n")
  532. config.plugins = False
  533. except RuntimeError as e:
  534. if verbose >= 1:
  535. sys.stderr.write("Error: %s\n" % str(e))
  536. sys.exit(1)
  537. # ------------------------------------------------
  538. if config.plugins:
  539. if verbose >= 2:
  540. print("+ Deploying plugins +")
  541. try:
  542. deployPlugins(applicationBundle, deploymentInfo, config.strip, verbose)
  543. except RuntimeError as e:
  544. if verbose >= 1:
  545. sys.stderr.write("Error: %s\n" % str(e))
  546. sys.exit(1)
  547. # ------------------------------------------------
  548. if len(config.add_qt_tr) == 0:
  549. add_qt_tr = []
  550. else:
  551. if translations_dir is not None:
  552. qt_tr_dir = translations_dir
  553. else:
  554. if deploymentInfo.qtPath is not None:
  555. qt_tr_dir = os.path.join(deploymentInfo.qtPath, "translations")
  556. else:
  557. sys.stderr.write("Error: Could not find Qt translation path\n")
  558. sys.exit(1)
  559. add_qt_tr = ["qt_%s.qm" % lng for lng in config.add_qt_tr[0].split(",")]
  560. for lng_file in add_qt_tr:
  561. p = os.path.join(qt_tr_dir, lng_file)
  562. if verbose >= 3:
  563. print("Checking for \"%s\"..." % p)
  564. if not os.path.exists(p):
  565. if verbose >= 1:
  566. sys.stderr.write("Error: Could not find Qt translation file \"%s\"\n" % (lng_file))
  567. sys.exit(1)
  568. # ------------------------------------------------
  569. if verbose >= 2:
  570. print("+ Installing qt.conf +")
  571. f = open(os.path.join(applicationBundle.resourcesPath, "qt.conf"), "wb")
  572. f.write(qt_conf.encode())
  573. f.close()
  574. # ------------------------------------------------
  575. if len(add_qt_tr) > 0 and verbose >= 2:
  576. print("+ Adding Qt translations +")
  577. for lng_file in add_qt_tr:
  578. if verbose >= 3:
  579. print(os.path.join(qt_tr_dir, lng_file), "->", os.path.join(applicationBundle.resourcesPath, lng_file))
  580. shutil.copy2(os.path.join(qt_tr_dir, lng_file), os.path.join(applicationBundle.resourcesPath, lng_file))
  581. # ------------------------------------------------
  582. if len(config.add_resources) > 0 and verbose >= 2:
  583. print("+ Adding additional resources +")
  584. for p in config.add_resources:
  585. t = os.path.join(applicationBundle.resourcesPath, os.path.basename(p))
  586. if verbose >= 3:
  587. print(p, "->", t)
  588. if os.path.isdir(p):
  589. shutil.copytree(p, t, symlinks=True)
  590. else:
  591. shutil.copy2(p, t)
  592. # ------------------------------------------------
  593. if config.sign and 'CODESIGNARGS' not in os.environ:
  594. print("You must set the CODESIGNARGS environment variable. Skipping signing.")
  595. elif config.sign:
  596. if verbose >= 1:
  597. print("Code-signing app bundle %s"%(target,))
  598. subprocess.check_call("codesign --force %s %s"%(os.environ['CODESIGNARGS'], target), shell=True)
  599. # ------------------------------------------------
  600. if config.dmg is not None:
  601. #Patch in check_output for Python 2.6
  602. if "check_output" not in dir( subprocess ):
  603. def f(*popenargs, **kwargs):
  604. if 'stdout' in kwargs:
  605. raise ValueError('stdout argument not allowed, it will be overridden.')
  606. process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs)
  607. output, unused_err = process.communicate()
  608. retcode = process.poll()
  609. if retcode:
  610. cmd = kwargs.get("args")
  611. if cmd is None:
  612. cmd = popenargs[0]
  613. raise CalledProcessError(retcode, cmd)
  614. return output
  615. subprocess.check_output = f
  616. def runHDIUtil(verb, image_basename, **kwargs):
  617. hdiutil_args = ["hdiutil", verb, image_basename + ".dmg"]
  618. if "capture_stdout" in kwargs:
  619. del kwargs["capture_stdout"]
  620. run = subprocess.check_output
  621. else:
  622. if verbose < 2:
  623. hdiutil_args.append("-quiet")
  624. elif verbose >= 3:
  625. hdiutil_args.append("-verbose")
  626. run = subprocess.check_call
  627. for key, value in kwargs.items():
  628. hdiutil_args.append("-" + key)
  629. if not value is True:
  630. hdiutil_args.append(str(value))
  631. return run(hdiutil_args)
  632. if verbose >= 2:
  633. if fancy is None:
  634. print("+ Creating .dmg disk image +")
  635. else:
  636. print("+ Preparing .dmg disk image +")
  637. if config.dmg != "":
  638. dmg_name = config.dmg
  639. else:
  640. spl = app_bundle_name.split(" ")
  641. dmg_name = spl[0] + "".join(p.capitalize() for p in spl[1:])
  642. if fancy is None:
  643. try:
  644. runHDIUtil("create", dmg_name, srcfolder="dist", format="UDBZ", volname=volname, ov=True)
  645. except subprocess.CalledProcessError as e:
  646. sys.exit(e.returncode)
  647. else:
  648. if verbose >= 3:
  649. print("Determining size of \"dist\"...")
  650. size = 0
  651. for path, dirs, files in os.walk("dist"):
  652. for file in files:
  653. size += os.path.getsize(os.path.join(path, file))
  654. size += int(size * 0.15)
  655. if verbose >= 3:
  656. print("Creating temp image for modification...")
  657. try:
  658. runHDIUtil("create", dmg_name + ".temp", srcfolder="dist", format="UDRW", size=size, volname=volname, ov=True)
  659. except subprocess.CalledProcessError as e:
  660. sys.exit(e.returncode)
  661. if verbose >= 3:
  662. print("Attaching temp image...")
  663. try:
  664. output = runHDIUtil("attach", dmg_name + ".temp", readwrite=True, noverify=True, noautoopen=True, capture_stdout=True)
  665. except subprocess.CalledProcessError as e:
  666. sys.exit(e.returncode)
  667. m = re.search("/Volumes/(.+$)", output)
  668. disk_root = m.group(0)
  669. disk_name = m.group(1)
  670. if verbose >= 2:
  671. print("+ Applying fancy settings +")
  672. if "background_picture" in fancy:
  673. bg_path = os.path.join(disk_root, ".background", os.path.basename(fancy["background_picture"]))
  674. os.mkdir(os.path.dirname(bg_path))
  675. if verbose >= 3:
  676. print(fancy["background_picture"], "->", bg_path)
  677. shutil.copy2(fancy["background_picture"], bg_path)
  678. else:
  679. bg_path = None
  680. if fancy.get("applications_symlink", False):
  681. os.symlink("/Applications", os.path.join(disk_root, "Applications"))
  682. # The Python appscript package broke with OSX 10.8 and isn't being fixed.
  683. # So we now build up an AppleScript string and use the osascript command
  684. # to make the .dmg file pretty:
  685. appscript = Template( """
  686. on run argv
  687. tell application "Finder"
  688. tell disk "$disk"
  689. open
  690. set current view of container window to icon view
  691. set toolbar visible of container window to false
  692. set statusbar visible of container window to false
  693. set the bounds of container window to {$window_bounds}
  694. set theViewOptions to the icon view options of container window
  695. set arrangement of theViewOptions to not arranged
  696. set icon size of theViewOptions to $icon_size
  697. $background_commands
  698. $items_positions
  699. close -- close/reopen works around a bug...
  700. open
  701. update without registering applications
  702. delay 5
  703. eject
  704. end tell
  705. end tell
  706. end run
  707. """)
  708. itemscript = Template('set position of item "${item}" of container window to {${position}}')
  709. items_positions = []
  710. if "items_position" in fancy:
  711. for name, position in fancy["items_position"].items():
  712. params = { "item" : name, "position" : ",".join([str(p) for p in position]) }
  713. items_positions.append(itemscript.substitute(params))
  714. params = {
  715. "disk" : volname,
  716. "window_bounds" : "300,300,800,620",
  717. "icon_size" : "96",
  718. "background_commands" : "",
  719. "items_positions" : "\n ".join(items_positions)
  720. }
  721. if "window_bounds" in fancy:
  722. params["window.bounds"] = ",".join([str(p) for p in fancy["window_bounds"]])
  723. if "icon_size" in fancy:
  724. params["icon_size"] = str(fancy["icon_size"])
  725. if bg_path is not None:
  726. # Set background file, then call SetFile to make it invisible.
  727. # (note: making it invisible first makes set background picture fail)
  728. bgscript = Template("""set background picture of theViewOptions to file ".background:$bgpic"
  729. do shell script "SetFile -a V /Volumes/$disk/.background/$bgpic" """)
  730. params["background_commands"] = bgscript.substitute({"bgpic" : os.path.basename(bg_path), "disk" : params["disk"]})
  731. s = appscript.substitute(params)
  732. if verbose >= 2:
  733. print("Running AppleScript:")
  734. print(s)
  735. p = subprocess.Popen(['osascript', '-'], stdin=subprocess.PIPE)
  736. p.communicate(input=s)
  737. if p.returncode:
  738. print("Error running osascript.")
  739. if verbose >= 2:
  740. print("+ Finalizing .dmg disk image +")
  741. time.sleep(5)
  742. try:
  743. runHDIUtil("convert", dmg_name + ".temp", format="UDBZ", o=dmg_name + ".dmg", ov=True)
  744. except subprocess.CalledProcessError as e:
  745. sys.exit(e.returncode)
  746. os.unlink(dmg_name + ".temp.dmg")
  747. # ------------------------------------------------
  748. if verbose >= 2:
  749. print("+ Done +")
  750. sys.exit(0)