diff --git a/builders/builder.py b/builders/builder.py new file mode 100644 index 0000000..b1ed5c7 --- /dev/null +++ b/builders/builder.py @@ -0,0 +1,475 @@ +# -*- 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 # +############################################################################### +""" +Base class for the Windows and macOS builders. +""" +import os +import sys +from argparse import ArgumentParser +from configparser import ConfigParser +from shutil import copy, rmtree +from subprocess import Popen, PIPE + +BUILDER_DESCRIPTION = 'Build OpenLP for {platform}. Options are provided on both the command line and a ' \ + 'configuration file. Options in the configuration file are overridden by the command line options.\n\n' \ + 'This build system can produce either development or release builds. A development release uses the ' \ + 'code as-is in the specified branch directory. The release build exports a tag from bzr and uses the ' \ + 'exported code for building. The two modes are invoked by the presence or absence of the --release ' \ + 'option. If this option is omitted, a development build is built, while including the --release ' \ + 'option with a version number will produce a build of that exact version.' + + +def _which(program): + """ + Return absolute path to a command found on system PATH. + """ + def is_exe(fpath): + return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + + fpath, fname = os.path.split(program) + if fpath and is_exe(os.path.abspath(program)): + return os.path.abspath(program) + else: + for path in os.environ['PATH'].split(os.pathsep): + path = path.strip('"') + exe_file = os.path.join(path, program) + if is_exe(exe_file): + return exe_file + return None + + +class Builder(object): + """ + A Generic class to base other operating system specific builders on + """ + def __init__(self): + self.setup_args() + self.setup_system_paths() + self.read_config() + self.setup_executables() + self.setup_paths() + self.setup_extra() + + def _print(self, text, *args): + """ + Print stuff out. Later we might want to use a log file. + """ + if len(args) > 0: + text = text % tuple(args) + print(text) + + def _print_verbose(self, text, *args): + """ + Print output, obeying "verbose" mode. + """ + if self.args.verbose: + self._print(text, *args) + + def _run_command(self, cmd, err_msg, exit_code=0): + """ + Run command in subprocess and print error message in case of Exception. + + Return text from stdout. + """ + proc = Popen(cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True) + output, error = proc.communicate() + code = proc.wait() + if code != exit_code: + self._print(output) + self._print(error) + raise Exception(err_msg) + return output, error + + def _bzr(self, command, work_path, args=[], err_msg='There was an error running bzr'): + """ + Update the code in the branch. + """ + os.chdir(work_path) + output, _ = self._run_command(['bzr', command] + args, err_msg) + return output + + def get_platform(self): + """ + Return the platform we're building for + """ + return 'unspecified' + + def get_config_defaults(self): + """ + Build some default values for the config file + """ + return {'here': os.path.dirname(self.config_path)} + + def get_sphinx_build(self): + """ + Get the type of build we should be running for Sphinx. Defaults to html. + """ + return 'html' + + def get_qt_translations_path(self): + """ + Return the path to Qt's translation files + """ + return '' + + def add_extra_args(self, parser): + """ + Add extra arguments to the argument parser + """ + pass + + def setup_args(self): + """ + Set up an argument parser and parse the command line arguments. + """ + parser = ArgumentParser(description=BUILDER_DESCRIPTION.format(platform=self.get_platform())) + parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', default=False, + help='Print out additional information') + parser.add_argument('-c', '--config', metavar='FILENAME', required=True, + help='Specify the path to the configuration file') + parser.add_argument('-b', '--branch', metavar='PATH', help='Specify the path to the branch you wish to build') + parser.add_argument('-r', '--release', metavar='VERSION', default=None, + help='Build a release version of OpenLP with the version specified') + parser.add_argument('-d', '--documentation', metavar='PATH', default=None, + help='Specify the path to the documentation branch') + parser.add_argument('-t', '--update-translations', action='store_true', default=False, + help='Update the translations from Transifex') + parser.add_argument('-u', '--transifex-user', metavar='USERNAME', default=None, help='Transifex username') + parser.add_argument('-p', '--transifex-pass', metavar='PASSWORD', default=None, help='Transifex password') + parser.add_argument('--skip-update', action='store_true', default=False, + help='Do NOT update the branch before building') + parser.add_argument('--skip-translations', action='store_true', default=False, + help='Do NOT update the language translation files') + self.add_extra_args(parser) + self.args = parser.parse_args() + + def read_config(self): + """ + Read the configuration from the configuration file. + """ + self.config = ConfigParser(defaults=self.get_config_defaults()) + self.config.read(self.config_path) + + def setup_system_paths(self): + """ + Set up some system paths. + """ + self.python = sys.executable + self.script_path = os.path.dirname(os.path.abspath(__file__)) + self.config_path = os.path.abspath(self.args.config) + self._print_verbose('System paths:') + self._print_verbose(' {:.<20}: {}'.format('python: ', self.python)) + self._print_verbose(' {:.<20}: {}'.format('script: ', self.script_path)) + self._print_verbose(' {:.<20}: {}'.format('config: ', self.config_path)) + + def setup_executables(self): + """ + Set up the paths to the executables we use. + """ + self._print_verbose('Executables:') + for executable in self.config.options('executables'): + path = self.config.get('executables', executable) + if not path.strip(): + path = None + else: + path = _which(path) + setattr(self, '{exe}_exe'.format(exe=executable), path) + self._print_verbose(' {exe:.<20} {path}'.format(exe=executable + ': ', path=path)) + + def setup_paths(self): + """ + Set up a variety of paths that we use throughout the build process. + """ + self._print_verbose('Paths:') + for name in self.config.options('paths'): + path = os.path.abspath(self.config.get('paths', name)) + setattr(self, '{name}_path'.format(name=name), path) + self._print_verbose(' {name:.<20} {path}'.format(name=name + ': ', path=path)) + # Make any command line options override the config file + if self.args.branch: + self.branch_path = os.path.abspath(self.args.branch) + if self.args.documentation: + self.documentation_path = os.path.abspath(self.args.documentation) + if self.args.release: + self.version = self.args.release + self.work_path = os.path.abspath(os.path.join(self.branch_path, '..', 'OpenLP-' + self.version)) + else: + self.version = None + self.work_path = self.branch_path + self.openlp_script = os.path.abspath(os.path.join(self.work_path, 'openlp-run.py')) + self.source_path = os.path.join(self.work_path, 'openlp') + self.manual_path = os.path.join(self.documentation_path, 'manual') + self.manual_build_path = os.path.join(self.manual_path, 'build') + self.i18n_utils = os.path.join(self.work_path, 'scripts', 'translation_utils.py') + self.i18n_path = os.path.join(self.work_path, 'resources', 'i18n') + self.build_path = os.path.join(self.work_path, 'build') + # Print out all the values + self._print_verbose(' {:.<20} {}'.format('openlp script: ', self.openlp_script)) + self._print_verbose(' {:.<20} {}'.format('source: ', self.source_path)) + self._print_verbose(' {:.<20} {}'.format('manual path: ', self.manual_path)) + self._print_verbose(' {:.<20} {}'.format('manual build path: ', self.manual_build_path)) + self._print_verbose(' {:.<20} {}'.format('i18n utils: ', self.i18n_utils)) + self._print_verbose(' {:.<20} {}'.format('i18n path: ', self.i18n_path)) + self._print_verbose(' {:.<20} {}'.format('build path: ', self.build_path)) + self._print_verbose('Overrides:') + self._print_verbose(' {:.<20} {}'.format('branch **: ', self.branch_path)) + self._print_verbose(' {:.<20} {}'.format('documentation **: ', self.branch_path)) + self._print_verbose(' {:.<20} {}'.format('version: ', self.version)) + self._print_verbose(' {:.<20} {}'.format('work path: ', self.work_path)) + + def setup_extra(self): + """ + Extra setup to run + """ + pass + + def update_code(self): + """ + Update the code in the branch. + """ + self._print('Reverting any changes to the code...') + self._bzr('revert', self.branch_path, err_msg='Error reverting the code') + self._print('Updating the code...') + self._bzr('update', self.branch_path, err_msg='Error updating the code') + + def export_release(self): + """ + Export a particular release + """ + if os.path.exists(self.work_path): + rmtree(self.work_path) + self._print('Exporting the release version...') + self._bzr('export', self.branch_path, ['-r', 'tag:' + self.version, self.work_path], + 'Error exporting the code') + + def run_pyinstaller(self): + """ + Run PyInstaller on the branch to build an executable. + """ + self._print('Running PyInstaller...') + copy(os.path.join(self.work_path, 'openlp.py'), self.openlp_script) + os.chdir(self.work_path) + cmd = [self.python, + self.pyinstaller_exe, + '--clean', + '--noconfirm', + '--windowed', + '--noupx', + '--additional-hooks-dir', self.hooks_path, + '--runtime-hook', os.path.join(self.hooks_path, 'rthook_ssl.py'), + '-i', self.icon_path, + '-n', 'OpenLP', + self.openlp_script] + if not self.args.verbose: + cmd.append('--log-level=ERROR') + else: + cmd.append('--log-level=DEBUG') + if not self.args.release: + cmd.append('-d') + self._print_verbose('... {}'.format(' '.join(cmd))) + output, error = self._run_command(cmd, 'Error running PyInstaller') + self._print_verbose(output) + self._print_verbose(error) + + def write_version_file(self): + """ + Write the version number to a file for reading once installed. + """ + self._print('Writing version file...') + if not self.args.release: + # This is a development build, get the tag and revision + output = self._bzr('tags', self.branch_path, err_msg='Error running bzr tags') + lines = output.splitlines() + if len(lines) == 0: + tag = '0.0.0' + revision = '0' + else: + tag, revision = lines[-1].split() + output = self._bzr('log', self.branch_path, ['--line', '-r', '-1'], 'Error running bzr log') + revision = output.split(':')[0] + self.version = '{tag}-bzr{revision}'.format(tag=tag, revision=revision) + # Write the version to the version file + with open(os.path.join(self.dist_path, '.version'), 'w') as version_file: + version_file.write(str(self.version)) + + def copy_default_theme(self): + """ + Copy the default theme to the correct directory for OpenLP. + """ + self._print('Copying default theme...') + source = os.path.join(self.source_path, 'core', 'lib', 'json') + dest = os.path.join(self.dist_path, 'core', 'lib', 'json') + for root, _, files in os.walk(source): + for filename in files: + if filename.endswith('.json'): + dest_path = os.path.join(dest, root[len(source) + 1:]) + if not os.path.exists(dest_path): + os.makedirs(dest_path) + self._print_verbose('... %s', filename) + copy(os.path.join(root, filename), os.path.join(dest_path, filename)) + + def copy_plugins(self): + """ + Copy all the plugins to the correct directory so that OpenLP sees that + it has plugins. + """ + self._print('Copying plugins...') + source = os.path.join(self.source_path, 'plugins') + dest = os.path.join(self.dist_path, 'plugins') + for root, _, files in os.walk(source): + for filename in files: + if not filename.endswith('.pyc'): + dest_path = os.path.join(dest, root[len(source) + 1:]) + if not os.path.exists(dest_path): + os.makedirs(dest_path) + self._print_verbose('... %s', filename) + copy(os.path.join(root, filename), os.path.join(dest_path, filename)) + + def copy_media_player(self): + """ + Copy the media players to the correct directory for OpenLP. + """ + self._print('Copying media player...') + source = os.path.join(self.source_path, 'core', 'ui', 'media') + dest = os.path.join(self.dist_path, 'core', 'ui', 'media') + for root, _, files in os.walk(source): + for filename in files: + if not filename.endswith('.pyc'): + dest_path = os.path.join(dest, root[len(source) + 1:]) + if not os.path.exists(dest_path): + os.makedirs(dest_path) + self._print_verbose('... %s', filename) + copy(os.path.join(root, filename), os.path.join(dest_path, filename)) + + def copy_extra_files(self): + """ + Copy any extra files which are particular to a platform + """ + pass + + def update_translations(self): + """ + Update the translations. + """ + self._print('Updating translations...') + username = None + password = None + if self.args.transifex_user: + username = self.args.transifex_user + if self.args.transifex_password: + password = self.args.transifex_pass + if (not username or not password) and not self.config.has_section('transifex'): + raise Exception('No section named "transifex" found.') + elif not username and not self.config.has_option('transifex', 'username'): + raise Exception('No option named "username" found.') + elif not password and not self.config.has_option('transifex', 'password'): + raise Exception('No option named "password" found.') + if not username: + username = self.config.get('transifex', 'username') + if not password: + password = self.config.get('transifex', 'password') + os.chdir(os.path.split(self.i18n_utils)[0]) + self._run_command([self.python, self.i18n_utils, '-qdpu', '-U', username, '-P', password], + err_msg='Error running translation_utils.py') + + def compile_translations(self): + """ + Compile the translations for Qt. + """ + self._print('Compiling translations...') + if not os.path.exists(os.path.join(self.dist_path, 'i18n')): + os.makedirs(os.path.join(self.dist_path, 'i18n')) + for filename in os.listdir(self.i18n_path): + if filename.endswith('.ts'): + self._print_verbose('... %s', filename) + source_path = os.path.join(self.i18n_path, filename) + dest_path = os.path.join(self.dist_path, 'i18n', filename.replace('.ts', '.qm')) + self._run_command((self.lrelease_exe, '-compress', '-silent', source_path, '-qm', dest_path), + err_msg='Error running lconvert on %s' % source_path) + self._print('Copying Qt translation files...') + source = self.get_qt_translations_path() + for filename in os.listdir(source): + if filename.startswith('qt_') and filename.endswith('.qm'): + self._print_verbose('... %s', filename) + copy(os.path.join(source, filename), os.path.join(self.dist_path, 'i18n', filename)) + + def run_sphinx(self): + """ + Run Sphinx to build the manual + """ + self._print('Running Sphinx...') + self._print_verbose('... Deleting previous help manual build... %s', self.manual_build_path) + if os.path.exists(self.manual_build_path): + rmtree(self.manual_build_path) + os.chdir(self.manual_path) + sphinx_build = self.get_sphinx_build() + command = [self.sphinx_exe, '-b', sphinx_build, '-d', 'build/doctrees', 'source', 'build/{}'.format(sphinx_build)] + self._run_command(command, 'Error running Sphinx') + self.after_run_sphinx() + + def after_run_sphinx(self): + """ + Run some extra commands after sphinx. + """ + pass + + def build_package(self): + """ + Actually package the resultant build + """ + pass + + def main(self): + """ + The main function to run the builder. + """ + self._print_verbose('OpenLP main script: ......%s', self.openlp_script) + self._print_verbose('Script path: .............%s', self.script_path) + self._print_verbose('Branch path: .............%s', self.branch_path) + self._print_verbose('') + if not self.args.skip_update: + self.update_code() + if self.args.release: + self.export_release() + self.run_pyinstaller() + self.write_version_file() + self.copy_default_theme() + self.copy_plugins() + self.copy_media_player() + if os.path.exists(self.manual_path): + self.run_sphinx() + else: + self._print('') + self._print('WARNING: Documentation trunk not found') + self._print(' Help file will not be included in build') + self._print('') + self.copy_extra_files() + if not self.args.skip_translations: + if self.args.update_translations: + self.update_translations() + self.compile_translations() + self.build_package() + + self._print('Done.') + raise SystemExit() + + diff --git a/builders/macosx-builder.py b/builders/macosx-builder.py new file mode 100644 index 0000000..8213434 --- /dev/null +++ b/builders/macosx-builder.py @@ -0,0 +1,307 @@ +# -*- 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 _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 _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 _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 _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 + + 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) + + def get_platform(self): + """ + Return the plaform we're building for + """ + return 'Mac OS X' + + def get_sphinx_build(self): + """ + The type of build Sphinx should be doing + """ + return 'applehelp' + + def get_qt_translations_path(self): + """ + Return the path to Qt translation files on macOS + """ + from PyQt5.QtCore import QCoreApplication + qt_library_path = QCoreApplication.libraryPaths()[0] + return os.path.join(os.path.dirname(qt_library_path), 'translations') + + 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 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() + + +if __name__ == '__main__': + MacOSXBuilder().main() diff --git a/builders/windows-builder.py b/builders/windows-builder.py new file mode 100644 index 0000000..a0086f3 --- /dev/null +++ b/builders/windows-builder.py @@ -0,0 +1,345 @@ +# -*- 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 # +############################################################################### +""" +Windows Build Script +-------------------- + +This script is used to build the Windows binary and the accompanying installer. +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. + +Inno Setup 5 + Inno Setup should be installed into "C:\\%PROGRAMFILES%\\Inno Setup 5" + +Sphinx + This is used to build the documentation. The documentation trunk must be at + the same directory level as OpenLP trunk and named "documentation". + +HTML Help Workshop + This is used to create the help file. + +PyInstaller + PyInstaller should be a git clone of + https://github.com/matysek/pyinstaller branch develop + +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". + +Visual C++ 2008 Express Edition + This is to build pptviewlib.dll, the library for controlling the + PowerPointViewer. + +windows-builder.py + This script, of course. It should be in the "windows-installer" directory + at the same level as OpenLP trunk. + +psvince.dll + This dll is used during the actual install of OpenLP to check if OpenLP is + running on the users machine prior to the setup. If OpenLP is running, + the install will fail. The dll can be obtained from here: + + http://www.vincenzo.net/isxkb/index.php?title=PSVince + + The dll is presently included with this script. + +Mako + Mako Templates for Python. This package is required for building the + remote plugin. It can be installed by going to your + python_directory\\scripts\\.. and running "easy_install Mako". If you do not + have easy_install, the Mako package can be obtained here: + + http://www.makotemplates.org/download.html + +MuPDF + Required for PDF support in OpenLP. Download the windows build from + mupdf.com, extract it, and set the mutoolbin option in the config file to + point to mutool.exe. + +MediaInfo + Required for the media plugin. Download the 32-bit CLI windows build from + https://mediaarea.net/nn/MediaInfo/Download/Windows and set the + mediainfobin option in the config file to point to MediaInfo.exe. + +Portable App Builds + The following are required if you are planning to make a portable build of + OpenLP. The portable build conforms to the standards published by + PortableApps.com: + + http://portableapps.com/development/portableapps.com_format + + PortableApps.com Installer: + + http://portableapps.com/apps/development/portableapps.com_installer + + PortableApps.com Launcher: + + http://portableapps.com/apps/development/portableapps.com_launcher + + NSIS Portable (Unicode version): + + http://portableapps.com/apps/development/nsis_portable +""" + +import os +from distutils import dir_util +from shutil import copy, move, rmtree + +from builder import Builder + + +class WindowsBuilder(Builder): + """ + The :class:`WindowsBuilder` class encapsulates everything that is needed + to build a Windows installer. + """ + def _build_pptviewlib(self): + """ + Build the PowerPoint Viewer DLL using Visual Studio. + """ + self._print('Building PPTVIEWLIB.DLL...') + if not os.path.exists(self.vcbuild_exe): + self._print('... WARNING: vcbuild.exe was not found, skipping building pptviewlib.dll') + return + self._run_command([self.vcbuild_exe, '/rebuild', os.path.join(self.pptviewlib_path, 'pptviewlib.vcproj'), + 'Release|Win32'], 'Error building pptviewlib.dll') + copy(os.path.join(self.pptviewlib_path, 'Release', 'pptviewlib.dll'), self.pptviewlib_path) + + def _create_innosetup_file(self): + """ + Create an InnoSetup file pointing to the branch being built. + """ + self._print('Creating Inno Setup file...') + config_dir = os.path.dirname(self.config_path) + with open(os.path.join(config_dir, 'OpenLP.iss.default'), 'r') as input_file, \ + open(os.path.join(config_dir, 'OpenLP.iss'), 'w') as output_file: + content = input_file.read() + content = content.replace('%(branch)s', self.branch_path) + content = content.replace('%(display_version)s', self.version.replace('-bzr', '.')) + output_file.write(content) + + def _run_innosetup(self): + """ + Run InnoSetup to create an installer. + """ + self._print('Running Inno Setup...') + config_dir = os.path.dirname(self.config_path) + os.chdir(config_dir) + self._run_command([self.innosetup_exe, os.path.join(config_dir, 'OpenLP.iss'), '/q'], + 'Error running InnoSetup') + + def _create_portableapp_structure(self): + """ + Checks the PortableApp directory structure amd creates + missing subdirs + """ + self._print('... Checking PortableApps directory structure...') + launcher_path = os.path.join(self.portable_dest_path, 'App', 'Appinfo', 'Launcher') + if not os.path.exists(launcher_path): + os.makedirs(launcher_path) + settings_path = os.path.join(self.portable_dest_path, 'Data', 'Settings') + if not os.path.exists(settings_path): + os.makedirs(settings_path) + + def _create_portableapps_appinfo_file(self): + """ + Create a Portabbleapps appinfo.ini file. + """ + self._print_verbose('... Creating PortableApps appinfo file ...') + config_dir = os.path.dirname(self.config_path) + if '-bzr' in self.version: + version, revision = self.version.split('-bzr') + version = version + '.0' * (2 - version.count('.')) + self.portable_version = version + '.' + revision + else: + self.portable_version = self.version + '.0' * (3 - self.version.count('.')) + with open(os.path.join(config_dir, 'appinfo.ini.default'), 'r') as input_file, \ + open(os.path.join(self.portable_dest_path, 'App', 'Appinfo', 'appinfo.ini'), 'w') as output_file: + content = input_file.read() + content = content.replace('%(display_version)s', self.portable_version) + content = content.replace('%(package_version)s', self.portable_version) + output_file.write(content) + + def _run_portableapp_builder(self): + """ + Creates a portable installer. + 1 Copies the distribution to the portable apps directory + 2 Builds the PortableApps Launcher + 3 Builds the PortableApps Install + """ + self._print('Running PortableApps Builder...') + self._print_verbose('... Clearing old files') + # Remove previous contents of portableapp build directory. + if os.path.exists(self.portable_dest_path): + rmtree(self.portable_dest_path) + self._print_verbose('... Creating PortableApps build directory') + # Copy the contents of the OpenLPPortable directory to the portable + # build directory. + dir_util.copy_tree(self.portable_source_path, self.portable_dest_path) + self._create_portableapp_structure() + self._create_portableapps_appinfo_file() + # Copy distribution files to portableapp build directory. + self._print_verbose('... Copying distribution files') + portable_app_path = os.path.join(self.portable_dest_path, 'App', 'OpenLP') + dir_util.copy_tree(self.dist_path, portable_app_path) + # Copy help files to portableapp build directory. + if os.path.isfile(os.path.join(self.helpfile_path, 'OpenLP.chm')): + self._print_verbose('... Copying help files') + dir_util.copy_tree(self.helpfile_path, os.path.join(portable_app_path, 'help')) + else: + self._print('... WARNING: Windows help file not found') + # Build the launcher. + self._print_verbose('... Building PortableApps Launcher') + self._run_command([self.portablelauncher_exe, self.portable_dest_path], + 'Error creating PortableApps Launcher') + # Build the portable installer. + self._print_verbose('... Building PortableApps Installer') + self._run_command([self.portableinstaller_exe, self.portable_dest_path], + 'Error running PortableApps Installer') + portable_exe_name = 'OpenLPPortable_%s.paf.exe' % self.portable_version + portable_exe_path = os.path.abspath(os.path.join(self.portable_dest_path, '..', portable_exe_name)) + self._print_verbose('... Portable Build: {}'.format(portable_exe_path)) + if os.path.exists(portable_exe_path): + move(portable_exe_path, os.path.join(self.dist_path, '..', portable_exe_name)) + self._print('PortableApp build complete') + else: + raise Exception('PortableApp failed to build') + + def get_platform(self): + """ + Return the platform we're building for + """ + return 'Windows' + + def get_config_defaults(self): + """ + Build some default values for the config file + """ + config_defaults = super().get_config_defaults() + config_defaults.update({ + 'pyroot': self.python_root, + 'progfiles': self.program_files, + 'sitepackages': self.site_packages, + 'projects': os.path.abspath(os.path.join(self.script_path, '..', '..')) + }) + return config_defaults + + def get_sphinx_build(self): + """ + Tell Sphinx we want to build HTML help + """ + return "htmlhelp" + + def get_qt_translations_path(self): + """ + Return the path to Qt translation files on macOS + """ + return os.path.join(self.site_packages, 'PyQt5', 'translations') + + def add_extra_args(self, parser): + """ + Add extra arguments to the command line argument parser + """ + parser.add_argument('--portable', action='store_true', default=False, + help='Build a PortableApps.com build of OpenLP too') + + def setup_system_paths(self): + """ + Set up some system paths. + """ + super().setup_system_paths() + self.python_root = os.path.dirname(self.python) + self.site_packages = os.path.join(self.python_root, 'Lib', 'site-packages') + self.program_files = os.getenv('PROGRAMFILES') + + def setup_paths(self): + """ + Set up a variety of paths that we use throughout the build process. + """ + super().setup_paths() + self.dist_path = os.path.join(self.work_path, 'dist', 'OpenLP') + self.helpfile_path = os.path.join(self.manual_build_path, 'htmlhelp') + self.winres_path = os.path.join(self.branch_path, 'resources', 'windows') + self.pptviewlib_path = os.path.join(self.source_path, 'plugins', 'presentations', 'lib', 'pptviewlib') + + def copy_extra_files(self): + """ + Copy all the Windows-specific files. + """ + self._print('Copying extra files for Windows...') + self._print_verbose('... OpenLP.ico') + copy(self.icon_path, os.path.join(self.dist_path, 'OpenLP.ico')) + self._print_verbose('... LICENSE.txt') + copy(self.license_path, os.path.join(self.dist_path, 'LICENSE.txt')) + self._print_verbose('... psvince.dll') + copy(self.psvince_exe, os.path.join(self.dist_path, 'psvince.dll')) + if os.path.isfile(os.path.join(self.helpfile_path, 'OpenLP.chm')): + self._print_verbose('... OpenLP.chm') + copy(os.path.join(self.helpfile_path, 'OpenLP.chm'), os.path.join(self.dist_path, 'OpenLP.chm')) + else: + self._print('... WARNING: Windows help file not found') + self._print_verbose('... mutool.exe') + if self.mutool_exe and os.path.isfile(self.mutool_exe): + copy(os.path.join(self.mutool_exe), os.path.join(self.dist_path, 'mutool.exe')) + else: + self._print('... WARNING: mutool.exe not found') + self._print_verbose('... MediaInfo.exe') + if self.mediainfo_exe and os.path.isfile(self.mediainfo_exe): + copy(os.path.join(self.mediainfo_exe), os.path.join(self.dist_path, 'MediaInfo.exe')) + else: + self._print('... WARNING: MediaInfo.exe not found') + + def after_run_sphinx(self): + """ + Run HTML Help Workshop to convert the Sphinx output into a manual. + """ + self._print('Running HTML Help Workshop...') + os.chdir(os.path.join(self.manual_build_path, 'htmlhelp')) + self._run_command([self.htmlhelp_exe, 'OpenLP.chm'], 'Error running HTML Help Workshop', exit_code=1) + + def build_package(self): + """ + Build the installer + """ + self._build_pptviewlib() + self._create_innosetup_file() + self._run_innosetup() + if self.args.portable: + self._run_portableapp_builder() + + +if __name__ == '__main__': + WindowsBuilder().main() diff --git a/osx/macosx-builder.py b/osx/macosx-builder.py deleted file mode 100644 index 57ef00d..0000000 --- a/osx/macosx-builder.py +++ /dev/null @@ -1,717 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2015 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 -import subprocess -import sys -from shutil import copy, copytree, rmtree -from subprocess import Popen, PIPE -from configparser import ConfigParser -from argparse import ArgumentParser - -from macholib.MachO import MachO -from macholib.util import flipwritable, in_system_path - - -def _which(command): - """ - Return absolute path to a command found on system PATH. - """ - if command.startswith('/'): - return command - for path in os.environ["PATH"].split(os.pathsep): - if os.access(os.path.join(path, command), os.X_OK): - return "%s/%s" % (path, command) - - -class MacosxBuilder(object): - """ - The :class:`MacosxBuilder` class encapsulates everything that is needed - to build a Mac OS X .dmg file. - """ - - def __init__(self): - self.setup_args() - self.setup_system_paths() - self.read_config() - self.setup_executables() - self.setup_paths() - - def _print(self, text, *args): - """ - Print stuff out. Later we might want to use a log file. - """ - if len(args) > 0: - text = text % tuple(args) - print(text) - - def _print_verbose(self, text, *args): - """ - Print output, obeying "verbose" mode. - """ - if self.args.verbose: - self._print(text, *args) - - def _run_command(self, cmd, err_msg): - """ - Run command in subprocess and print error message in case of Exception. - - Return text from stdout. - """ - proc = Popen(cmd, stdout=PIPE, stderr=PIPE) - output, error = proc.communicate() - code = proc.wait() - if code != 0: - self._print(output) - self._print(error) - raise Exception(err_msg) - return output - - 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_mountpoints(self): - """ - Return list of mounted disks on Mac. - """ - # Get the output in plist format. - paths = [] - output = self._run_command([self.hdiutil, 'info', '-plist'], 'Detecting mount points failed.') - pl = plistlib.readPlistFromBytes(output) - for image in pl['images']: - for se in image['system-entities']: - if se.get('mount-point'): - paths.append(se.get('mount-point')) - - return paths - - def setup_args(self): - """ - Set up an argument parser and parse the command line arguments. - """ - parser = ArgumentParser() - parser.add_argument('-b', '--branch', metavar='BRANCH', dest='branch', - help='Specify the path to the branch you wish to build.') - parser.add_argument('--devel', dest='devel', action='store_true', default=False, - help='Development build does not have set icons for .dmg file ' - 'and .dmg filename contains bzr revision number.') - parser.add_argument('--release', dest='release', metavar='VERSION', - help='Build a release version of OpenLP with the version specified') - parser.add_argument('-d', '--documentation', metavar='DOCS', dest='docs', - help='Specify the path to the documentation branch.') - parser.add_argument('-c', '--config', metavar='CONFIG', dest='config', - help='Specify the path to the configuration file.', - default=os.path.abspath(os.path.join('.', 'config.ini.default'))) - parser.add_argument('-u', '--skip-update', dest='skip_update', action='store_true', default=False, - help='Do NOT update the branch before building.') - parser.add_argument('-t', '--skip-translations', dest='skip_translations', action='store_true', default=False, - help='Do NOT update the language translation files.') - parser.add_argument('--transifex', dest='update_translations', action='store_true', default=False, - help='Update the language translation from Transifex.') - parser.add_argument('--transifex-user', dest='transifex_user', help='Transifex username.') - parser.add_argument('--transifex-pass', dest='transifex_pass', help='Transifex password.') - parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', default=False, - help='Print out additional information.') - self.args = parser.parse_args() - - def read_config(self): - """ - Read the configuration from the configuration file. - """ - self.config = ConfigParser(defaults={ - 'here': self.script_path, - 'projects': os.path.abspath(os.path.join(self.script_path, '..', '..')), }) - self.config.read(os.path.abspath(self.args.config)) - - def setup_system_paths(self): - """ - Set up some system paths. - """ - self.script_path = os.path.dirname(os.path.abspath(__file__)) - self.python = sys.executable - - def setup_executables(self): - """ - Set up the paths to the executables we use. - """ - self.sphinx = _which(self.config.get('executables', 'sphinx')) - self.pyinstaller = os.path.abspath(self.config.get('executables', 'pyinstaller')) - self.lrelease = self.config.get('executables', 'lrelease') - self.dmgbuild = _which(self.config.get('executables', 'dmgbuild')) - self.mudraw_bin = _which(self.config.get('executables', 'mudrawbin')) - self.mutool_bin = _which(self.config.get('executables', 'mutoolbin')) - if self.mutool_bin: - self.mutool_lib = os.path.abspath( - os.path.join(os.path.dirname(self.mutool_bin), '..', 'lib', 'libjbig2dec.0.dylib')) - - def setup_paths(self): - """ - Set up a variety of paths that we use throughout the build process. - """ - if self.args.branch: - self.branch_path = os.path.abspath(self.args.branch) - else: - self.branch_path = self.config.get('paths', 'branch') - if self.args.docs: - self.docs_path = os.path.abspath(self.args.docs) - else: - self.docs_path = self.config.get('paths', 'documentation') - if self.args.release: - self.version_number = self.args.release - self.work_path = os.path.abspath(os.path.join(self.branch_path, '..', 'OpenLP-' + self.version_number)) - else: - self.version_number = None - self.work_path = self.branch_path - self.openlp_script = os.path.abspath(os.path.join(self.work_path, 'openlp.py')) - self.hooks_path = os.path.abspath(os.path.join(self.work_path, self.config.get('paths', 'hooks'))) - self.app_icon = os.path.abspath(self.config.get('paths', 'app_icon')) - self.bundle_info = os.path.abspath(self.config.get('paths', 'bundle_info')) - self.i18n_utils = os.path.join(self.work_path, 'scripts', 'translation_utils.py') - self.source_path = os.path.join(self.work_path, 'openlp') - self.manual_path = os.path.join(self.docs_path, 'manual') - self.manual_build_path = os.path.join(self.manual_path, 'build') - self.i18n_path = os.path.join(self.work_path, 'resources', 'i18n') - self.build_path = os.path.join(self.work_path, 'build') - 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') - self.dmg_settings = os.path.abspath(self.config.get('paths', 'dmg_settings')) - - # Path to Qt translation files. - from PyQt5.QtCore import QCoreApplication - - qt_plug_dir = str(list(QCoreApplication.libraryPaths())[0]) - self.qt_translations_path = os.path.join(os.path.dirname(qt_plug_dir), 'translations') - - def update_code(self): - """ - Update the code in the branch. - """ - os.chdir(self.branch_path) - self._print('Reverting any changes to the code...') - bzr = Popen(('bzr', 'revert'), stdout=PIPE) - output = bzr.communicate()[0] - code = bzr.wait() - if code != 0: - self._print(output) - raise Exception('Error reverting the code') - self._print('Updating the code...') - bzr = Popen(('bzr', 'update'), stdout=PIPE) - output = bzr.communicate()[0] - code = bzr.wait() - if code != 0: - self._print(output) - raise Exception('Error updating the code') - - def export_release(self): - """ - Export a particular release - """ - if os.path.exists(self.work_path): - rmtree(self.work_path) - os.chdir(self.branch_path) - self._print('Exporting the release version...') - bzr = Popen(('bzr', 'export', '-r', 'tag:' + self.version_number, self.work_path), stdout=PIPE) - output = bzr.communicate()[0] - code = bzr.wait() - if code != 0: - self._print(output) - raise Exception('Error exporting the code') - - def run_pyinstaller(self): - """ - Run PyInstaller on the branch to build an executable. - """ - self._print('Running PyInstaller...') - os.chdir(self.work_path) - cmd = [self.python, - self.pyinstaller, - '--clean', - '--noconfirm', - '--windowed', - '--noupx', - '--additional-hooks-dir', self.hooks_path, - '--runtime-hook', os.path.join(self.hooks_path, 'rthook_ssl.py'), - '-i', self.app_icon, - '-n', 'OpenLP', - self.openlp_script] - if not self.args.verbose: - cmd.append('--log-level=ERROR') - else: - cmd.append('--log-level=DEBUG') - if self.args.devel: - cmd.append('-d') - pyinstaller = Popen(cmd) - code = pyinstaller.wait() - if code != 0: - raise Exception('Error running PyInstaller') - - def write_version_file(self): - """ - Write the version number to a file for reading once installed. - """ - self._print('Writing version file...') - os.chdir(self.branch_path) - bzr = Popen(('bzr', 'tags'), stdout=PIPE) - output = bzr.communicate()[0] - code = bzr.wait() - if code != 0: - raise Exception('Error running bzr tags') - lines = output.splitlines() - if len(lines) == 0: - tag = '0.0.0' - revision = '0' - else: - tag, revision = lines[-1].decode('utf-8').split() - bzr = Popen(('bzr', 'log', '--line', '-r', '-1'), stdout=PIPE) - output, error = bzr.communicate() - code = bzr.wait() - if code != 0: - raise Exception('Error running bzr log') - latest = output.decode('utf-8').split(':')[0] - self.version_string = '%s-bzr%s' % (tag, latest) - self.version_tag = tag - version_file = open(os.path.join(self.dist_path, '.version'), 'w') - # Release version does not contain revision in .dmg name. - if self.args.devel: - version_file.write(str(self.version_string)) - else: - version_file.write(str(self.version_tag)) - version_file.close() - - def copy_default_theme(self): - """ - Copy the default theme to the correct directory for OpenLP. - """ - self._print('Copying default theme...') - source = os.path.join(self.source_path, 'core', 'lib', 'json') - dest = os.path.join(self.dist_path, 'core', 'lib', 'json') - for root, dirs, files in os.walk(source): - for filename in files: - if filename.endswith('.json'): - dest_path = os.path.join(dest, root[len(source) + 1:]) - if not os.path.exists(dest_path): - os.makedirs(dest_path) - self._print_verbose('... %s', filename) - copy(os.path.join(root, filename), os.path.join(dest_path, filename)) - - def copy_plugins(self): - """ - Copy all the plugins to the correct directory so that OpenLP sees that - it has plugins. - """ - self._print('Copying plugins...') - source = os.path.join(self.source_path, 'plugins') - dest = os.path.join(self.dist_path, 'plugins') - for root, dirs, files in os.walk(source): - for filename in files: - if not filename.endswith('.pyc'): - dest_path = os.path.join(dest, root[len(source) + 1:]) - if not os.path.exists(dest_path): - os.makedirs(dest_path) - self._print_verbose('... %s', filename) - copy(os.path.join(root, filename), os.path.join(dest_path, filename)) - - def copy_media_player(self): - """ - Copy the media players to the correct directory for OpenLP. - """ - self._print('Copying media player...') - source = os.path.join(self.source_path, 'core', 'ui', 'media') - dest = os.path.join(self.dist_path, 'core', 'ui', 'media') - for root, dirs, files in os.walk(source): - for filename in files: - if not filename.endswith('.pyc'): - dest_path = os.path.join(dest, root[len(source) + 1:]) - if not os.path.exists(dest_path): - os.makedirs(dest_path) - self._print_verbose('... %s', filename) - copy(os.path.join(root, filename), os.path.join(dest_path, filename)) - - def copy_mac_bundle_files(self): - """ - Copy Info.plist and OpenLP.icns to app bundle. - """ - copy(self.app_icon, os.path.join(self.dist_app_path, 'Contents', 'Resources', os.path.basename(self.app_icon))) - # Add OpenLP version to Info.plist and put it to app bundle. - fr = open(self.bundle_info, 'r') - fw = open(os.path.join(self.dist_app_path, 'Contents', os.path.basename(self.bundle_info)), 'w') - text = fr.read() - if self.args.devel: - text = text % {'openlp_version': self.version_string} - else: - text = text % {'openlp_version': self.version_tag} - 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(os.path.join(self.script_path, 'LICENSE.txt'), os.path.join(self.dist_path, 'LICENSE.txt')) - self._print_verbose('... mudraw') - if self.mudraw_bin and os.path.isfile(self.mudraw_bin): - copy(self.mudraw_bin, os.path.join(self.dist_path, 'mudraw')) - self.relink_mudraw() - elif self.mutool_bin and os.path.isfile(self.mutool_bin): - copy(self.mutool_bin, 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') - f = open(dll.filename, 'rb+') - for header in dll.headers: - f.seek(0) - dll.write(f) - f.seek(0, 2) - f.flush() - f.close() - except Exception: - pass - - def update_translations(self): - """ - Update the translations. - """ - self._print('Updating translations...') - if not self.config.has_section('transifex'): - raise Exception('No section named "transifex" found.') - if not self.config.has_option('transifex', 'username'): - raise Exception('No option named "username" found.') - if not self.config.has_option('transifex', 'password'): - raise Exception('No option named "password" found.') - if self.args.transifex_user: - username = self.args.transifex_user - else: - username = self.config.get('transifex', 'username') - if self.args.transifex_pass: - password = self.args.transifex_pass - else: - password = self.config.get('transifex', 'password') - os.chdir(os.path.split(self.i18n_utils)[0]) - translation_utils = Popen([self.python, self.i18n_utils, '-qdpu', '-U', username, '-P', password]) - code = translation_utils.wait() - if code != 0: - raise Exception('Error running translation_utils.py') - - def compile_translations(self): - """ - Compile the translations for Qt. - """ - self._print('Compiling translations...') - files = os.listdir(self.i18n_path) - if not os.path.exists(os.path.join(self.dist_path, 'i18n')): - os.makedirs(os.path.join(self.dist_path, 'i18n')) - for file in files: - if file.endswith('.ts'): - self._print_verbose('... %s', file) - source_path = os.path.join(self.i18n_path, file) - dest_path = os.path.join(self.dist_path, 'i18n', file.replace('.ts', '.qm')) - lconvert = Popen((self.lrelease, '-compress', '-silent', source_path, '-qm', dest_path)) - code = lconvert.wait() - if code != 0: - raise Exception('Error running lconvert on %s' % source_path) - self._print('Copying qm files...') - source = self.qt_translations_path - files = os.listdir(source) - for filename in files: - if filename.startswith('qt_') and filename.endswith('.qm'): - self._print_verbose('... %s', filename) - copy(os.path.join(source, filename), os.path.join(self.dist_path, 'i18n', filename)) - - def run_sphinx(self): - """ - Run Sphinx to build an HTML Help project. - """ - self._print('Deleting previous manual build... %s', self.manual_build_path) - if os.path.exists(self.manual_build_path): - rmtree(self.manual_build_path) - self._print('Running Sphinx...') - os.chdir(self.manual_path) - sphinx = Popen((self.sphinx, '-b', 'applehelp', '-d', 'build/doctrees', 'source', 'build/applehelp'), - stdout=PIPE) - output, error = sphinx.communicate() - code = sphinx.wait() - if code != 0: - self._print(output) - raise Exception('Error running Sphinx') - 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 code_sign(self): - certificate = self.config.get('codesigning', 'certificate') - self._print('Checking for certificate...') - security = Popen(('security', 'find-certificate', '-c', certificate), - stdout=PIPE) - output, error = security.communicate() - code = security.wait() - if code != 0: - self._print('Could not find certificate \"%s\" in Keychain...', certificate) - self._print('Codesigning will not work without a certificate!!') - self._print(output) - else: - self._print('Codesigning app...') - codesign = Popen(('codesign', '--deep', '-s', certificate, self.dist_app_path)) - output, error = codesign.communicate() - code = codesign.wait() - if code != 0: - self._print(output) - raise Exception('Error running codesign') - - def create_dmg_file(self): - """ - Create .dmg file. - """ - self._print('Creating dmg file...') - - # Release version does not contain revision in .dmg name. - if self.args.devel: - dmg_name = 'OpenLP-' + str(self.version_string) + '.dmg' - dmg_title = 'OpenLP {version}'.format(version=self.version_string) - else: - dmg_name = 'OpenLP-' + str(self.version_tag) + '.dmg' - dmg_title = 'OpenLP {version}'.format(version=self.version_tag) - - 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) - # Create empty dmg file. - size = self._get_directory_size(self.dist_app_path) # in bytes. - size = size / (1000 * 1000) # Convert to megabytes. - size += 10 # Additional space in .dmg for other files. - - self._print('... %s' % self.script_path) - os.chdir(self.script_path) - self._run_command([self.dmgbuild, '-s', self.dmg_settings, '-D', 'size={size}M'.format(size=size), - '-D', 'icon={icon_path}'.format(icon_path=self.app_icon), - '-D', 'app={dist_app_path}'.format(dist_app_path=self.dist_app_path), dmg_title, self.dmg_file], - 'Unable to run dmgbuild') - - # Jenkins integration. - # Continuous integration server needs to know the filename of dmg. - # Write java property file. For uploading dmg to openlp. - if self.args.devel: - fpath = os.path.join(self.branch_path, 'openlp.properties') - self._print('... writing property file for jenkins: %s' % fpath) - f = open(fpath, 'w') - f.write('OPENLP_DMGNAME=' + os.path.basename(self.dmg_file) + '\n') - f.close() - - # Dmg done. - self._print('Finished creating dmg file, resulting file: %s' % self.dmg_file) - - def main(self): - """ - The main function to run the Mac OS X builder. - """ - self._print_verbose('OpenLP main script: ......%s', self.openlp_script) - self._print_verbose('Script path: .............%s', self.script_path) - self._print_verbose('Branch path: .............%s', self.branch_path) - self._print_verbose('Source path: .............%s', self.source_path) - self._print_verbose('"dist.app" path: .........%s', self.dist_app_path) - self._print_verbose('"dist" path: .............%s', self.dist_path) - self._print_verbose('"hooks" path: ............%s', self.hooks_path) - self._print_verbose('PyInstaller: .............%s', self.pyinstaller) - self._print_verbose('dmgbuild: ................%s', self.dmgbuild) - self._print_verbose('Documentation branch path:%s', self.docs_path) - if self.mudraw_bin: - self._print_verbose('mudraw binary ............%s', self.mudraw_bin) - elif self.mutool_bin: - self._print_verbose('mutool binary ............%s', self.mutool_bin) - else: - self._print_verbose('mutool/mudraw ............Not found') - self._print_verbose('') - if not self.args.skip_update: - self.update_code() - if self.args.release: - self.export_release() - self.run_pyinstaller() - self.write_version_file() - self.copy_mac_bundle_files() - self.copy_default_theme() - self.copy_plugins() - self.copy_media_player() - # TODO creating help on Mac - if os.path.exists(self.manual_path): - self.run_sphinx() - else: - self._print('') - self._print('WARNING: Documentation trunk not found. Mac OS X') - self._print(' Help file will not be included in build') - self._print('') - self.copy_macosx_files() - if not self.args.skip_translations: - if self.args.update_translations: - self.update_translations() - self.compile_translations() - self.code_sign() - self.create_dmg_file() - - self._print('Done.') - raise SystemExit() - - -if __name__ == '__main__': - MacosxBuilder().main() diff --git a/windows/config-appveyor.ini b/windows/config-appveyor.ini index 73f548b..e98aba7 100644 --- a/windows/config-appveyor.ini +++ b/windows/config-appveyor.ini @@ -1,22 +1,24 @@ [executables] innosetup = %(progfiles)s\Inno Setup 5\ISCC.exe sphinx = %(pyroot)s\Scripts\sphinx-build.exe -pyinstaller = %(here)s\..\..\pyinstaller-develop\pyinstaller.py +pyinstaller = %(here)s\..\..\PyInstaller-3.2\pyinstaller.py vcbuild = %(progfiles)s\Microsoft Visual Studio 9.0\VC\vcpackages\vcbuild.exe htmlhelp = %(progfiles)s\HTML Help Workshop\hhc.exe psvince = %(here)s\psvince.dll lrelease = %(sitepackages)s\PyQt5\bin\lrelease.exe portablelauncher = %(here)s\..\..\PortableApps.comLauncher\PortableApps.comLauncherGenerator.exe portableinstaller = %(here)s\..\..\PortableApps.comInstaller\PortableApps.comInstaller.exe -mutoolbin = %(here)s\..\..\mupdf-1.9a-windows\mutool.exe -mediainfobin = %(here)s\..\..\MediaInfo\MediaInfo.exe +mutool = %(here)s\..\..\mupdf-1.9a-windows\mutool.exe +mediainfo = %(here)s\..\..\MediaInfo\MediaInfo.exe [paths] branch = %(projects)s\trunk documentation = %(projects)s\documentation -win32icon = %(here)s\OpenLP.ico +icon = %(here)s\OpenLP.ico hooks = %(here)s\..\pyinstaller-hooks -portable = %(projects)s\OpenLPPortable +license = %(here)s\LICENSE.txt +portable_source = %(here)s\OpenLPPortable +portable_dest = %(projects)s\OpenLPPortable [transifex] username = diff --git a/windows/config.ini.default b/windows/config.ini.default index 04c786b..d335ff4 100644 --- a/windows/config.ini.default +++ b/windows/config.ini.default @@ -8,15 +8,17 @@ psvince = %(here)s\psvince.dll lrelease = %(sitepackages)s\PyQt5\bin\lrelease.exe portablelauncher = %(progfiles)s\PortableApps.comLauncher\PortableApps.comLauncherGenerator.exe portableinstaller = %(progfiles)s\PortableApps.comInstaller\PortableApps.comInstaller.exe -mutoolbin = %(here)s\..\mupdf-1.9a-windows\mutool.exe -mediainfobin = %(here)s\..\MediaInfo\MediaInfo.exe +mutool = %(here)s\..\mupdf-1.9a-windows\mutool.exe +mediainfo = %(here)s\..\MediaInfo\MediaInfo.exe [paths] branch = %(projects)s\trunk documentation = %(projects)s\documentation -win32icon = %(here)s\OpenLP.ico +icon = %(here)s\OpenLP.ico hooks = %(here)s\..\pyinstaller-hooks -portable = %(projects)s\OpenLPPortable +license = %(here)s\LICENSE.txt +portable_source = %(here)s\OpenLPPortable +portable_dest = %(projects)s\OpenLPPortable [transifex] username = diff --git a/windows/windows-builder.py b/windows/windows-builder.py deleted file mode 100755 index ba6b994..0000000 --- a/windows/windows-builder.py +++ /dev/null @@ -1,653 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2015 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 # -############################################################################### - -""" -Windows Build Script --------------------- - -This script is used to build the Windows binary and the accompanying installer. -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. - -Inno Setup 5 - Inno Setup should be installed into "C:\%PROGRAMFILES%\Inno Setup 5" - -Sphinx - This is used to build the documentation. The documentation trunk must be at - the same directory level as OpenLP trunk and named "documentation". - -HTML Help Workshop - This is used to create the help file. - -PyInstaller - PyInstaller should be a git clone of - https://github.com/matysek/pyinstaller branch develop - -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". - -Visual C++ 2008 Express Edition - This is to build pptviewlib.dll, the library for controlling the - PowerPointViewer. - -windows-builder.py - This script, of course. It should be in the "windows-installer" directory - at the same level as OpenLP trunk. - -psvince.dll - This dll is used during the actual install of OpenLP to check if OpenLP is - running on the users machine prior to the setup. If OpenLP is running, - the install will fail. The dll can be obtained from here: - - http://www.vincenzo.net/isxkb/index.php?title=PSVince) - - The dll is presently included with this script. - -Mako - Mako Templates for Python. This package is required for building the - remote plugin. It can be installed by going to your - python_directory\scripts\.. and running "easy_install Mako". If you do not - have easy_install, the Mako package can be obtained here: - - http://www.makotemplates.org/download.html - -MuPDF - Required for PDF support in OpenLP. Download the windows build from - mupdf.com, extract it, and set the mutoolbin option in the config file to - point to mutool.exe. - -MediaInfo - Required for the media plugin. Download the 32-bit CLI windows build from - https://mediaarea.net/nn/MediaInfo/Download/Windows and set the - mediainfobin option in the config file to point to MediaInfo.exe. - -Portable App Builds - The following are required if you are planning to make a portable build of - OpenLP. The portable build conforms to the standards published by - PortableApps.com: - - http://portableapps.com/development/portableapps.com_format - - PortableApps.com Installer: - - http://portableapps.com/apps/development/portableapps.com_installer - - PortableApps.com Launcher: - - http://portableapps.com/apps/development/portableapps.com_launcher - - NSIS Portable (Unicode version): - - http://portableapps.com/apps/development/nsis_portable -""" - -import os -import sys -from shutil import copy, rmtree, move -from distutils import dir_util -from subprocess import Popen, PIPE -from configparser import ConfigParser -from argparse import ArgumentParser - - -class WindowsBuilder(object): - """ - The :class:`WindowsBuilder` class encapsulates everything that is needed - to build a Windows installer. - """ - - def __init__(self): - self.setup_args() - self.setup_system_paths() - self.read_config() - self.setup_executables() - self.setup_paths() - self.version = '' - - def _print(self, text, *args): - """ - Print stuff out. Later we might want to use a log file. - """ - if len(args) > 0: - text = text % tuple(args) - print(text) - - def _print_verbose(self, text, *args): - """ - Print output, obeying "verbose" mode. - """ - if self.args.verbose: - self._print(text, *args) - - def setup_args(self): - """ - Set up an argument parser and parse the command line arguments. - """ - parser = ArgumentParser() - parser.add_argument('-b', '--branch', metavar='BRANCH', dest='branch', - help='Specify the path to the branch you wish to build.', default=None) - parser.add_argument('-d', '--documentation', metavar='DOCS', dest='docs', default=None, - help='Specify the path to the documentation branch.') - parser.add_argument('-c', '--config', metavar='CONFIG', dest='config', - help='Specify the path to the configuration file.', - default=os.path.abspath(os.path.join('.', 'config.ini'))) - parser.add_argument('-u', '--skip-update', dest='skip_update', action='store_true', default=False, - help='Do NOT update the branch before building.') - parser.add_argument('-p', '--portable', metavar='PORTABLE', dest='portable', default=None, - help='Specify the path to build the portable installation.') - parser.add_argument('-t', '--skip-translations', dest='skip_translations', action='store_true', default=False, - help='Do NOT update the language translation files.') - parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', default=False, - help='Print out additional information.') - self.args = parser.parse_args() - - def read_config(self): - """ - Read the configuration from the configuration file. - """ - self.config = ConfigParser(defaults={ - 'pyroot': self.python_root, - 'progfiles': self.program_files, - 'sitepackages': self.site_packages, - 'here': self.script_path, - 'projects': os.path.abspath(os.path.join(self.script_path, '..', '..')), - }) - self.config.read(os.path.abspath(self.args.config)) - - def setup_system_paths(self): - """ - Set up some system paths. - """ - self.script_path = os.path.dirname(os.path.abspath(__file__)) - self.python = sys.executable - self.python_root = os.path.dirname(self.python) - self.site_packages = os.path.join(self.python_root, 'Lib', 'site-packages') - self.program_files = os.getenv('PROGRAMFILES') - - def setup_executables(self): - """ - Set up the paths to the executables we use. - """ - self.innosetup = os.path.abspath(self.config.get('executables', 'innosetup')) - self.sphinx = os.path.abspath(self.config.get('executables', 'sphinx')) - self.pyinstaller = os.path.abspath(self.config.get('executables', 'pyinstaller')) - self.vcbuild = os.path.abspath(self.config.get('executables', 'vcbuild')) - self.hhc = os.path.abspath(self.config.get('executables', 'htmlhelp')) - self.psvince = os.path.abspath(self.config.get('executables', 'psvince')) - self.portableinstaller = os.path.abspath(self.config.get('executables', 'portableinstaller')) - self.portablelauncher = os.path.abspath(self.config.get('executables', 'portablelauncher')) - self.mutool_bin = os.path.abspath(self.config.get('executables', 'mutoolbin')) - self.mediainfo_bin = os.path.abspath(self.config.get('executables', 'mediainfobin')) - if os.path.exists(os.path.join(self.site_packages, 'PyQt5', 'bin')): - # Older versions of the PyQt5 Windows installer put their binaries - # in the "bin" directory - self.lrelease = os.path.join(self.site_packages, 'PyQt5', 'bin', 'lrelease.exe') - else: - # Newer versions of the PyQt5 Windows installer put their binaries - # in the base directory of the installation - self.lrelease = os.path.join(self.site_packages, 'PyQt5', 'lrelease.exe') - - def setup_paths(self): - """ - Set up a variety of paths that we use throughout the build process. - """ - if self.args.branch: - branch_path = self.args.branch - else: - branch_path = self.config.get('paths', 'branch') - self.branch_path = os.path.abspath(branch_path) - if self.args.docs: - docs_path = self.args.docs - else: - docs_path = self.config.get('paths', 'documentation') - self.docs_path = os.path.abspath(docs_path) - if self.args.portable: - portable_path = self.args.portable - else: - try: - portable_path = self.config.get('paths', 'portable') - except: - portable_path = '' - if portable_path: - self.portable_path = os.path.abspath(portable_path) - self.args.portable = self.portable_path - else: - self.portable_path = '' - self.openlp_script = os.path.abspath(os.path.join(branch_path, 'openlp.py')) - self.hooks_path = os.path.abspath(self.config.get('paths', 'hooks')) - self.win32_icon = os.path.abspath(self.config.get('paths', 'win32icon')) - self.i18n_utils = os.path.join(self.branch_path, 'scripts', 'translation_utils.py') - self.source_path = os.path.join(self.branch_path, 'openlp') - self.manual_path = os.path.join(self.docs_path, 'manual') - self.manual_build_path = os.path.join(self.manual_path, 'build') - self.helpfile_path = os.path.join(self.manual_build_path, 'htmlhelp') - self.i18n_path = os.path.join(self.branch_path, 'resources', 'i18n') - self.winres_path = os.path.join(self.branch_path, 'resources', 'windows') - self.build_path = os.path.join(self.branch_path, 'build') - self.dist_path = os.path.join(self.branch_path, 'dist', 'OpenLP') - self.dist_path_pyinst_arg = os.path.join(self.branch_path, 'dist') - self.pptviewlib_path = os.path.join(self.source_path, 'plugins', 'presentations', 'lib', 'pptviewlib') - - def update_code(self): - """ - Update the code in the branch. - """ - os.chdir(self.branch_path) - self._print('Reverting any changes to the code...') - bzr = Popen(('bzr', 'revert'), stdout=PIPE) - output = bzr.communicate()[0] - code = bzr.wait() - if code != 0: - self._print(output) - raise Exception('Error reverting the code') - self._print('Updating the code...') - bzr = Popen(('bzr', 'update'), stdout=PIPE) - output = bzr.communicate()[0] - code = bzr.wait() - if code != 0: - self._print(output) - raise Exception('Error updating the code') - - def run_pyinstaller(self): - """ - Run PyInstaller on the branch to build an executable. - """ - self._print('Running PyInstaller...') - os.chdir(self.branch_path) - cmd = [self.python, - self.pyinstaller, - '--clean', - '--noconfirm', - '--windowed', - '--noupx', - '--additional-hooks-dir', self.hooks_path, - '--distpath', self.dist_path_pyinst_arg, - '-i', self.win32_icon, - '-p', self.branch_path, - '-n', 'OpenLP', - self.openlp_script] - if not self.args.verbose: - cmd.append('--log-level=ERROR') - else: - cmd.append('--log-level=DEBUG') - pyinstaller = Popen(cmd) - code = pyinstaller.wait() - if code != 0: - raise Exception('Error running PyInstaller') - - def write_version_file(self): - """ - Write the version number to a file for reading once installed. - """ - self._print('Writing version file...') - os.chdir(self.branch_path) - bzr = Popen(('bzr', 'tags'), stdout=PIPE) - output = bzr.communicate()[0] - code = bzr.wait() - if code != 0: - raise Exception('Error running bzr tags') - lines = output.splitlines() - if len(lines) == 0: - tag = '0.0.0' - revision = '0' - else: - tag, revision = lines[-1].decode('utf-8').split() - bzr = Popen(('bzr', 'log', '--line', '-r', '-1'), stdout=PIPE) - output, error = bzr.communicate() - code = bzr.wait() - if code != 0: - raise Exception('Error running bzr log') - latest = output.decode('utf-8').split(':')[0] - version_string = latest == revision and tag or '%s-bzr%s' % (tag, latest) - # Save decimal version in case we need to do a portable build. - self.version = latest == revision and tag or '%s.%s' % (tag, latest) - version_file = open(os.path.join(self.dist_path, '.version'), 'w') - version_file.write(str(version_string)) - version_file.close() - - def copy_default_theme(self): - """ - Copy the default theme to the correct directory for OpenLP. - """ - self._print('Copying default theme...') - source = os.path.join(self.source_path, 'core', 'lib', 'json') - dest = os.path.join(self.dist_path, 'core', 'lib', 'json') - for root, dirs, files in os.walk(source): - for filename in files: - if filename.endswith('.json'): - dest_path = os.path.join(dest, root[len(source) + 1:]) - if not os.path.exists(dest_path): - os.makedirs(dest_path) - self._print_verbose('... %s', filename) - copy(os.path.join(root, filename), os.path.join(dest_path, filename)) - - def copy_plugins(self): - """ - Copy all the plugins to the correct directory so that OpenLP sees that - it has plugins. - """ - self._print('Copying plugins...') - source = os.path.join(self.source_path, 'plugins') - dest = os.path.join(self.dist_path, 'plugins') - for root, dirs, files in os.walk(source): - for filename in files: - if not filename.endswith('.pyc'): - dest_path = os.path.join(dest, root[len(source) + 1:]) - if not os.path.exists(dest_path): - os.makedirs(dest_path) - self._print_verbose('... %s', filename) - copy(os.path.join(root, filename), os.path.join(dest_path, filename)) - - def copy_media_player(self): - """ - Copy the media players to the correct directory for OpenLP. - """ - self._print('Copying media player...') - source = os.path.join(self.source_path, 'core', 'ui', 'media') - dest = os.path.join(self.dist_path, 'core', 'ui', 'media') - for root, dirs, files in os.walk(source): - for filename in files: - if not filename.endswith('.pyc'): - dest_path = os.path.join(dest, root[len(source) + 1:]) - if not os.path.exists(dest_path): - os.makedirs(dest_path) - self._print_verbose('... %s', filename) - copy(os.path.join(root, filename), os.path.join(dest_path, filename)) - - def copy_windows_files(self): - """ - Copy all the Windows-specific files. - """ - self._print('Copying extra files for Windows...') - self._print_verbose('... OpenLP.ico') - copy(os.path.join(self.script_path, 'OpenLP.ico'), os.path.join(self.dist_path, 'OpenLP.ico')) - self._print_verbose('... LICENSE.txt') - copy(os.path.join(self.script_path, 'LICENSE.txt'), os.path.join(self.dist_path, 'LICENSE.txt')) - self._print_verbose('... psvince.dll') - copy(self.psvince, os.path.join(self.dist_path, 'psvince.dll')) - if os.path.isfile(os.path.join(self.helpfile_path, 'OpenLP.chm')): - self._print_verbose('... OpenLP.chm') - copy(os.path.join(self.helpfile_path, 'OpenLP.chm'), os.path.join(self.dist_path, 'OpenLP.chm')) - else: - self._print('... WARNING: Windows help file not found') - self._print_verbose('... mutool.exe') - if self.mutool_bin and os.path.isfile(self.mutool_bin): - copy(os.path.join(self.mutool_bin), os.path.join(self.dist_path, 'mutool.exe')) - else: - self._print('... WARNING: mutool.exe not found') - self._print_verbose('... MediaInfo.exe') - if self.mediainfo_bin and os.path.isfile(self.mediainfo_bin): - copy(os.path.join(self.mediainfo_bin), os.path.join(self.dist_path, 'MediaInfo.exe')) - else: - self._print('... WARNING: MediaInfo.exe not found') - - def update_translations(self): - """ - Update the translations. - """ - self._print('Updating translations...') - if not self.config.has_section('transifex'): - raise Exception('No section named "transifex" found.') - if not self.config.has_option('transifex', 'username'): - raise Exception('No option named "username" found.') - if not self.config.has_option('transifex', 'password'): - raise Exception('No option named "password" found.') - username = self.config.get('transifex', 'username') - password = self.config.get('transifex', 'password') - os.chdir(os.path.dirname(self.i18n_utils)) - translation_utils = Popen([self.python, self.i18n_utils, '-qdpu', '-U', username, '-P', password]) - code = translation_utils.wait() - if code != 0: - raise Exception('Error running translation_utils.py') - - def compile_translations(self): - """ - Compile the translations for Qt. - """ - self._print('Compiling translations...') - files = os.listdir(self.i18n_path) - if not os.path.exists(os.path.join(self.dist_path, 'i18n')): - os.makedirs(os.path.join(self.dist_path, 'i18n')) - for file in files: - if file.endswith('.ts'): - self._print_verbose('... %s', file) - source_path = os.path.join(self.i18n_path, file) - dest_path = os.path.join(self.dist_path, 'i18n', file.replace('.ts', '.qm')) - lconvert = Popen((self.lrelease, '-compress', '-silent', source_path, '-qm', dest_path)) - code = lconvert.wait() - if code != 0: - raise Exception('Error running lconvert on %s' % source_path) - self._print('Copying qm files...') - source = os.path.join(self.site_packages, 'PyQt5', 'translations') - files = os.listdir(source) - for filename in files: - if filename.startswith('qt_') and filename.endswith('.qm') and len(filename) == 8: - self._print_verbose('... %s', filename) - copy(os.path.join(source, filename), os.path.join(self.dist_path, 'i18n', filename)) - - def run_sphinx(self): - """ - Run Sphinx to build an HTML Help project. - """ - self._print('Deleting previous help manual build... %s', self.manual_build_path) - if os.path.exists(self.manual_build_path): - rmtree(self.manual_build_path) - self._print('Running Sphinx...') - os.chdir(self.manual_path) - sphinx = Popen((self.sphinx, '-b', 'htmlhelp', '-d', 'build/doctrees', 'source', 'build/htmlhelp'), stdout=PIPE) - output, error = sphinx.communicate() - code = sphinx.wait() - if code != 0: - self._print(output) - raise Exception('Error running Sphinx') - - def run_htmlhelp(self): - """ - Run HTML Help Workshop to convert the Sphinx output into a manual. - """ - self._print('Running HTML Help Workshop...') - os.chdir(os.path.join(self.manual_build_path, 'htmlhelp')) - hhc = Popen((self.hhc, 'OpenLP.chm'), stdout=PIPE) - output, error = hhc.communicate() - code = hhc.wait() - if code != 1: - self._print('Exit code:', code) - self._print(output) - raise Exception('Error running HTML Help Workshop') - - def create_innosetup_file(self): - """ - Create an InnoSetup file pointing to the branch being built. - """ - self._print('Creating Inno Setup file...') - input = open(os.path.join(self.script_path, 'OpenLP.iss.default'), 'r').read() - output = input.replace('%(branch)s', self.branch_path) - output = output.replace('%(display_version)s', self.version) - outfile = open(os.path.join(self.script_path, 'OpenLP.iss'), 'w') - outfile.write(output) - outfile.close() - - def check_portableapp_directory(self): - """ - Checks the PortableApp directory structure amd creates - missing subdirs - """ - self._print(' Checking PortableApps directory structure...') - launcher_path = os.path.join(self.portable_path, 'App', 'Appinfo', 'Launcher') - if not os.path.exists(launcher_path): - os.makedirs(launcher_path) - settings_path = os.path.join(self.portable_path, 'Data', 'Settings') - if not os.path.exists(settings_path): - os.makedirs(settings_path) - - def create_portableapps_appinfo_file(self): - """ - Create a Portabbleapps appinfo.ini file. - """ - self._print(' Creating PortableApps appinfo file ...') - portable_version = self.version + '.0' * (3 - self.version.count('.')) - input = open(os.path.join(self.script_path, 'appinfo.ini.default'), 'r').read() - output = input.replace('%(display_version)s', self.version) - output = output.replace('%(package_version)s', portable_version) - outfile = open(os.path.join(self.portable_path, 'App', 'Appinfo', 'appinfo.ini'), 'w') - outfile.write(output) - outfile.close() - - def run_innosetup(self): - """ - Run InnoSetup to create an installer. - """ - self._print('Running Inno Setup...') - os.chdir(self.script_path) - innosetup = Popen((self.innosetup, os.path.join(self.script_path, 'OpenLP.iss'), '/q')) - code = innosetup.wait() - if code != 0: - raise Exception('Error running Inno Setup') - - def run_portableapp_builder(self): - """ - Creates a portable installer. - 1 Copies the distribution to the portable apps directory - 2 Builds the PortableApps Launcher - 3 Builds the PortableApps Install - """ - self._print('Running PortableApps Builder...') - self._print(' Clearing old files') - # Remove previous contents of portableapp build directory. - if os.path.exists(self.portable_path): - rmtree(self.portable_path) - self._print(' Creating PortableApps build directory') - # Copy the contents of the OpenLPPortable directory to the portable - # build directory. - dir_util.copy_tree(os.path.join(self.script_path, 'OpenLPPortable'), self.portable_path) - self.check_portableapp_directory() - self.create_portableapps_appinfo_file() - # Copy distribution files to portableapp build directory. - self._print(' Copying distribution files') - portable_app_path = os.path.join(self.portable_path, 'App', 'OpenLP') - dir_util.copy_tree(self.dist_path, portable_app_path) - # Copy help files to portableapp build directory. - if os.path.isfile(os.path.join(self.helpfile_path, 'OpenLP.chm')): - self._print(' Copying help files') - dir_util.copy_tree(self.helpfile_path, os.path.join(portable_app_path, 'help')) - else: - self._print('... WARNING: Windows help file not found') - # Build the launcher. - self._print(' Building PortableApps Launcher') - portableapps = Popen((self.portablelauncher, self.portable_path), stdout=PIPE) - code = portableapps.wait() - if code != 0: - raise Exception('Error creating PortableAppa Launcher') - # Build the portable installer. - self._print(' Building PortableApps Installer') - portableapps = Popen((self.portableinstaller, self.portable_path), stdout=PIPE) - code = portableapps.wait() - if code != 0: - raise Exception('Error running PortableApps Installer') - portable_app = os.path.abspath(os.path.join(self.portable_path, '..', - 'OpenLPPortable_%s.paf.exe' % self.version)) - if os.path.exists(portable_app): - move(portable_app, os.path.abspath(os.path.join(self.dist_path, '..'))) - self._print(' PortableApp build complete') - else: - raise Exception('PortableApp failed to build') - - def build_pptviewlib(self): - """ - Build the PowerPoint Viewer DLL using Visual Studio. - """ - self._print('Building PPTVIEWLIB.DLL...') - if not os.path.exists(self.vcbuild): - self._print('... WARNING: vcbuild.exe was not found, skipping building pptviewlib.dll') - return - vcbuild = Popen((self.vcbuild, '/rebuild', os.path.join(self.pptviewlib_path, 'pptviewlib.vcproj'), - 'Release|Win32')) - code = vcbuild.wait() - if code != 0: - raise Exception('Error building pptviewlib.dll') - copy(os.path.join(self.pptviewlib_path, 'Release', 'pptviewlib.dll'), self.pptviewlib_path) - - def main(self): - """ - The main function to run the Windows builder. - """ - self._print_verbose('OpenLP main script: ......%s', self.openlp_script) - self._print_verbose('Script path: .............%s', os.path.dirname(os.path.abspath(__file__))) - self._print_verbose('Branch path: .............%s', self.branch_path) - self._print_verbose('Source path: .............%s', self.source_path) - self._print_verbose('Dist path: ...............%s', self.dist_path) - self._print_verbose('Portable path: ...........%s', self.portable_path) - self._print_verbose('PyInstaller: .............%s', self.pyinstaller) - self._print_verbose('Documentation branch path:%s', self.docs_path) - self._print_verbose('Help file build path: ....%s', self.helpfile_path) - self._print_verbose('Inno Setup path: .........%s', self.innosetup) - self._print_verbose('PortableApp Launcher......%s', self.portablelauncher) - self._print_verbose('PortableApp Installer.....%s', self.portableinstaller) - self._print_verbose('Windows resources: .......%s', self.winres_path) - self._print_verbose('VCBuild path: ............%s', self.vcbuild) - self._print_verbose('PPTVIEWLIB path: .........%s', self.pptviewlib_path) - self._print_verbose('Mutool binary ............%s', self.mutool_bin) - self._print_verbose('') - if not self.args.skip_update: - self.update_code() - self.build_pptviewlib() - self.run_pyinstaller() - self.write_version_file() - self.copy_default_theme() - self.copy_plugins() - self.copy_media_player() - if os.path.exists(self.manual_path): - self.run_sphinx() - self.run_htmlhelp() - else: - self._print('') - self._print('WARNING: Documentation trunk not found. Windows') - self._print(' Help file will not be included in build') - self._print('') - self.copy_windows_files() - if not self.args.skip_translations: - self.update_translations() - self.compile_translations() - self.create_innosetup_file() - self.run_innosetup() - if self.args.portable: - self.run_portableapp_builder() - self._print('Done.') - - -if __name__ == '__main__': - WindowsBuilder().main()