1
0
mirror of https://gitlab.com/openlp/packaging.git synced 2024-12-22 21:12:50 +00:00

Refactor the Windowsand macOS builders to reuse common code. Fix the issue with dud builds when using PyInstaller 3.2.

bzr-revno: 33
This commit is contained in:
Raoul Snyman 2016-12-13 22:43:22 +02:00
commit 6759718d9b
7 changed files with 1140 additions and 1379 deletions

475
builders/builder.py Normal file
View File

@ -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()

307
builders/macosx-builder.py Normal file
View File

@ -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()

345
builders/windows-builder.py Normal file
View File

@ -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()

View File

@ -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()

View File

@ -1,22 +1,24 @@
[executables] [executables]
innosetup = %(progfiles)s\Inno Setup 5\ISCC.exe innosetup = %(progfiles)s\Inno Setup 5\ISCC.exe
sphinx = %(pyroot)s\Scripts\sphinx-build.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 vcbuild = %(progfiles)s\Microsoft Visual Studio 9.0\VC\vcpackages\vcbuild.exe
htmlhelp = %(progfiles)s\HTML Help Workshop\hhc.exe htmlhelp = %(progfiles)s\HTML Help Workshop\hhc.exe
psvince = %(here)s\psvince.dll psvince = %(here)s\psvince.dll
lrelease = %(sitepackages)s\PyQt5\bin\lrelease.exe lrelease = %(sitepackages)s\PyQt5\bin\lrelease.exe
portablelauncher = %(here)s\..\..\PortableApps.comLauncher\PortableApps.comLauncherGenerator.exe portablelauncher = %(here)s\..\..\PortableApps.comLauncher\PortableApps.comLauncherGenerator.exe
portableinstaller = %(here)s\..\..\PortableApps.comInstaller\PortableApps.comInstaller.exe portableinstaller = %(here)s\..\..\PortableApps.comInstaller\PortableApps.comInstaller.exe
mutoolbin = %(here)s\..\..\mupdf-1.9a-windows\mutool.exe mutool = %(here)s\..\..\mupdf-1.9a-windows\mutool.exe
mediainfobin = %(here)s\..\..\MediaInfo\MediaInfo.exe mediainfo = %(here)s\..\..\MediaInfo\MediaInfo.exe
[paths] [paths]
branch = %(projects)s\trunk branch = %(projects)s\trunk
documentation = %(projects)s\documentation documentation = %(projects)s\documentation
win32icon = %(here)s\OpenLP.ico icon = %(here)s\OpenLP.ico
hooks = %(here)s\..\pyinstaller-hooks 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] [transifex]
username = username =

View File

@ -8,15 +8,17 @@ psvince = %(here)s\psvince.dll
lrelease = %(sitepackages)s\PyQt5\bin\lrelease.exe lrelease = %(sitepackages)s\PyQt5\bin\lrelease.exe
portablelauncher = %(progfiles)s\PortableApps.comLauncher\PortableApps.comLauncherGenerator.exe portablelauncher = %(progfiles)s\PortableApps.comLauncher\PortableApps.comLauncherGenerator.exe
portableinstaller = %(progfiles)s\PortableApps.comInstaller\PortableApps.comInstaller.exe portableinstaller = %(progfiles)s\PortableApps.comInstaller\PortableApps.comInstaller.exe
mutoolbin = %(here)s\..\mupdf-1.9a-windows\mutool.exe mutool = %(here)s\..\mupdf-1.9a-windows\mutool.exe
mediainfobin = %(here)s\..\MediaInfo\MediaInfo.exe mediainfo = %(here)s\..\MediaInfo\MediaInfo.exe
[paths] [paths]
branch = %(projects)s\trunk branch = %(projects)s\trunk
documentation = %(projects)s\documentation documentation = %(projects)s\documentation
win32icon = %(here)s\OpenLP.ico icon = %(here)s\OpenLP.ico
hooks = %(here)s\..\pyinstaller-hooks 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] [transifex]
username = username =

View File

@ -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()