# -*- 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 lxml.etree import fromstring, tostring from lxml.builder import E from builder import Builder BLACKLIST = [ 'pptviewlib.dll.intermediate.manifest' ] 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 _walk_dirs(self, dir_dict, path): """ Walk a dictionary according to path """ parts = path.split(os.sep) search_key = parts.pop(0) if search_key in dir_dict.keys(): if not parts: return dir_dict[search_key] else: return walk_dirs(dir_dict[search_key], os.sep.join(parts)) else: return None def _get_fragments_from_files(self, start_dir): """ Walk down a directory recursively and build up the XML for WiX """ start_base, start_path = os.path.split(start_dir) element = E.DirectoryRef(Id='INSTALLDIR') directories = {start_path: {'__dir__': element}} components = [] for root, _, files in os.walk(start_dir): parent = os.sep.join(root.replace(os.path.join(start_base, ''), '').split(os.sep)[:-1]) if root == start_dir: path = '' else: path = root.replace(os.path.join(start_dir, ''), '') base = os.path.basename(root) if root != start_dir: dir_id = 'dir_{parent}_{base}'.format(parent=parent.replace(os.sep, '_'), base=base) element = E.Directory(Id=dir_id, Name=base) new_dir = {'__dir__': element} parent_dir = walk_dirs(directories, parent) parent_dir[base] = new_dir parent_dir['__dir__'].append(element) for fname in files: if fname in BLACKLIST: continue source = os.path.join(path, fname) if path else fname file_id = 'file_{source}'.format(source=source.replace('-', '_').replace(os.sep, '_')) file_ = E.File(Id=file_id, KeyPath="yes", Source=source) component = E.Component(file_, Id='cmp_' + fixed_id, Guid='*') element.append(component) components.append(component) files_fragment = E.Fragment(directories[start_path]['__dir__']) comps_fragment = E.Fragment(E.ComponentGroup(*[E.ComponentRef(Id=c.attrib['Id']) for c in components], Id='Files')) return files_fragment, comps_fragment def _create_wix_file(self): """ Create a WiX project file """ self._print('Creating WiX file...') config_dir = os.path.dirname(self.config_path) self._print_verbose('Reading base WiX file') with open(os.path.join(config_dir, 'OpenLP-base.wxs'), 'rb') as base_file: xml = base_file.read() xml = xml.format(dialog=os.path.join(config_dir, 'WizardMain.bmp'), banner=os.path.join(config_dir, 'WizardBanner.bmp')) tree = fromstring(xml) self._print_verbose('Creating XML fragments from files and directories') fragments = self._get_fragments_from_files(self.dist_path) self._print_verbose('Inserting XML fragments into base WiX file') wix = base_tree.getroot() for fragment in fragments: wix.append(fragment) self._print_verbose('Writing new WiX file') with open(os.path.join(self.config_path, 'OpenLP.wxs'), 'wb') as f: f.write(tostring(tree, encoding='utf-8', xml_declaration=True, pretty_print=True)) def _run_wix_tools(self): """ Run the WiX toolset to create an installer """ self._print('Running WiX tools...') msi_file = os.path.join(self.dist_path, 'OpenLP-{}.msi'.format(self.version)) if msi_file: self._print_verbose('Removing old MSI file') os.unlink(msi_file) config_dir = os.path.dirname(self.config_path) os.chdir(self.dist_path) self._run_command([self.candle_exe, '-ext', 'WiXUtilExtension', os.path.join(config_dir, 'OpenLP.wxs')], 'Error running WiX tool: candle') self._run_command([self.light_exe, '-ext', 'WiXUtilExtension', '-ext', 'WixUIExtension', 'OpenLP.wixobj', '-o', msi_file], 'Error running WiX tool: light') 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_wix_file() self._run_wix_tools() if self.args.portable: self._run_portableapp_builder() if __name__ == '__main__': WindowsBuilder().main()