# -*- coding: utf-8 -*- # vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 ############################################################################### # OpenLP - Open Source Lyrics Projection # # --------------------------------------------------------------------------- # # Copyright (c) 2004-2016 OpenLP Developers # # --------------------------------------------------------------------------- # # This program is free software; you can redistribute it and/or modify it # # under the terms of the GNU General Public License as published by the Free # # Software Foundation; version 2 of the License. # # # # This program is distributed in the hope that it will be useful, but WITHOUT # # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # # more details. # # # # You should have received a copy of the GNU General Public License along # # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ Mac OS X Build Script -------------------- This script is used to build the Mac OS X app bundle and pack it into dmg file. For this script to work out of the box, it depends on a number of things: Python 3.4 PyQt5 You should already have this installed, OpenLP doesn't work without it. The version the script expects is the packaged one available from River Bank Computing. PyEnchant This script expects the precompiled, installable version of PyEnchant to be installed. You can find this on the PyEnchant site. Sphinx This is used to build the documentation. The documentation trunk must be at the same directory level as OpenLP trunk and named "documentation". PyInstaller PyInstaller should be a git clone of either https://github.com/matysek/pyinstaller branch python3 or https://github.com/pyinstaller/pyinstaller branch python3 Bazaar You need the command line "bzr" client installed. OpenLP A checkout of the latest code, in a branch directory, which is in a Bazaar shared repository directory. This means your code should be in a directory structure like this: "openlp\branch-name". macosx-builder.py This script, of course. It should be in the "osx-package" directory at the same level as OpenLP trunk. Mako Mako Templates for Python. This package is required for building the remote plugin. Alembic Required for upgrading the databases used in OpenLP. MuPDF Required for PDF support in OpenLP. Install using macports, or use the mudrawbin option in the config file to point to the mudraw binary. MachOLib Python library to analyze and edit Mach-O headers, the executable format used by Mac OS X. Used to relink the mudraw binary from MuPDF to the bundled libraries. Install using macports or pip. config.ini.default The configuration file contains settings of the version string to include in the bundle as well as directory and file settings for different purposes (e.g. PyInstaller location or installer background image) To install everything you need to install MacPorts. Once MacPorts is installed and up-to-date, run the following command:: $ sudo port install python34 py34-pyqt4 py34-sphinx py34-sqlalchemy \ py34-macholib py34-mako py34-alembic py34-enchant \ py34-beautifulsoup4 py34-lxml py34-nose You may need to install chardet via pip:: $ sudo pip install chardet """ import os import plistlib import signal from shutil import copy, copytree from macholib.MachO import MachO from macholib.util import flipwritable, in_system_path from builder import Builder class MacosxBuilder(Builder): """ The :class:`MacosxBuilder` class encapsulates everything that is needed to build a Mac OS X .dmg file. """ def _get_directory_size(self, directory): """ Return directory size - size of everything in the dir. """ dir_size = 0 for (path, dirs, files) in os.walk(directory): for file in files: filename = os.path.join(path, file) dir_size += os.path.getsize(filename) return dir_size def get_sphinx_build(self): """ The type of build Sphinx should be doing """ return 'applehelp' def setup_paths(self): """ Extra setup to run """ super().setup_paths() if hasattr(self, 'mutool_exe'): self.mutool_lib = os.path.abspath( os.path.join(os.path.dirname(self.mutool_exe), '..', 'lib', 'libjbig2dec.0.dylib')) self.dist_app_path = os.path.join(self.work_path, 'dist', 'OpenLP.app') self.dist_path = os.path.join(self.work_path, 'dist', 'OpenLP.app', 'Contents', 'MacOS') def copy_extra_files(self): """ Copy any extra files which are particular to a platform """ self._copy_bundle_files() self._copy_macosx_files() def _copy_bundle_files(self): """ Copy Info.plist and OpenLP.icns to app bundle. """ copy(self.icon_path, os.path.join(self.dist_app_path, 'Contents', 'Resources', os.path.basename(self.icon_path))) # Add OpenLP version to Info.plist and put it to app bundle. fr = open(self.bundle_info_path, 'r') fw = open(os.path.join(self.dist_app_path, 'Contents', os.path.basename(self.bundle_info_path)), 'w') text = fr.read() text = text % {'openlp_version': self.version} fw.write(text) fr.close() fw.close() def _copy_macosx_files(self): """ Copy all the OSX-specific files. """ self._print('Copying extra files for Mac OS X...') self._print_verbose('... LICENSE.txt') copy(self.license_path, os.path.join(self.dist_path, 'LICENSE.txt')) self._print_verbose('... mudraw') if hasattr(self, 'mudraw_exe') and self.mudraw_exe and os.path.isfile(self.mudraw_exe): copy(self.mudraw_exe, os.path.join(self.dist_path, 'mudraw')) self.relink_mudraw() elif hasattr(self, 'mutool_exe') and self.mutool_exe and os.path.isfile(self.mutool_exe): copy(self.mutool_exe, os.path.join(self.dist_path, 'mutool')) self.relink_mutool() copy(self.mutool_lib, os.path.join(self.dist_path, 'libjbig2dec.0.dylib')) else: self._print('... WARNING: mudraw and mutool not found') def relink_mudraw(self): """ Relink mudraw to bundled libraries """ self.relink_mupdf('mudraw') def relink_mutool(self): """ Relink mudraw to bundled libraries """ self.relink_mupdf('mutool') def relink_mupdf(self, bin_name): """ Relink mupdf to bundled libraries """ self._print('Linking {bin_name} with bundled libraries...'.format(bin_name=bin_name)) libname = os.path.join(self.dist_path, bin_name) distname = os.path.relpath(self.dist_path, libname) self._print_verbose('... {bin_name} path {path}'.format(bin_name=bin_name, path=libname)) # Determine how many directories up is the directory with shared # dynamic libraries. '../' # E.g. ./qt4_plugins/images/ -> ./../../ parent_dir = '' # Check if distname is not only base filename. if os.path.dirname(distname): parent_level = len(os.path.dirname(distname).split(os.sep)) parent_dir = parent_level * (os.pardir + os.sep) def match_func(pth): """ For system libraries leave path unchanged. """ # Match non system dynamic libraries. if not in_system_path(pth): # Use relative path to dependend dynamic libraries bases on # location of the executable. pth = os.path.join('@loader_path', parent_dir, os.path.basename(pth)) self._print_verbose('... %s', pth) return pth # Rewrite mach headers with @loader_path. dll = MachO(libname) dll.rewriteLoadCommands(match_func) # Write changes into file. # Write code is based on macholib example. try: self._print_verbose('... writing new library paths') with open(dll.filename, 'rb+') as dll_file: for header in dll.headers: dll_file.seek(0) dll.write(dll_file) dll_file.seek(0, 2) except Exception: pass def after_run_sphinx(self): """ Run Sphinx to build an HTML Help project. """ self._print('Copying help file...') source = os.path.join(self.manual_build_path, 'applehelp') files = os.listdir(source) for filename in files: if filename.endswith('.help'): self._print_verbose('... %s', filename) copytree(os.path.join(source, filename), os.path.join(self.dist_app_path, 'Contents', 'Resources', filename)) def build_package(self): """ Build the actual DMG """ self.code_sign() self.create_dmg() def code_sign(self): certificate = self.config.get('codesigning', 'certificate') self._print('Checking for certificate...') self._run_command(['security', 'find-certificate', '-c', certificate], 'Could not find certificate "{certificate}" in keychain, '.format(certificate=certificate) + 'codesigning will not work without a certificate') self._print('Codesigning app...') self._run_command(['codesign', '--deep', '-s', certificate, self.dist_app_path], 'Error running codesign') def create_dmg(self): """ Create .dmg file. """ self._print('Creating dmg file...') dmg_name = 'OpenLP-{version}.dmg'.format(version=self.version) dmg_title = 'OpenLP {version}'.format(version=self.version) self.dmg_file = os.path.join(self.work_path, 'dist', dmg_name) # Remove dmg if it exists. if os.path.exists(self.dmg_file): os.remove(self.dmg_file) # Get size of the directory in bytes, convert to MB, and add padding size = self._get_directory_size(self.dist_app_path) size = size / (1000 * 1000) size += 10 self._print('... %s' % self.script_path) os.chdir(os.path.dirname(self.dmg_settings_path)) self._run_command([self.dmgbuild_exe, '-s', self.dmg_settings_path, '-D', 'size={size}M'.format(size=size), '-D', 'icon={icon_path}'.format(icon_path=self.icon_path), '-D', 'app={dist_app_path}'.format(dist_app_path=self.dist_app_path), dmg_title, self.dmg_file], 'Unable to run dmgbuild') # Dmg done. self._print('Finished creating dmg file, resulting file: %s' % self.dmg_file) if __name__ == '__main__': MacosxBuilder().main()