mirror of
https://gitlab.com/openlp/packaging.git
synced 2024-09-01 03:33:42 +00:00
494 lines
23 KiB
Python
494 lines
23 KiB
Python
# -*- coding: utf-8 -*-
|
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
|
|
|
##########################################################################
|
|
# OpenLP - Open Source Lyrics Projection #
|
|
# ---------------------------------------------------------------------- #
|
|
# Copyright (c) 2008-2019 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, either version 3 of the License, or #
|
|
# (at your option) any later version. #
|
|
# #
|
|
# 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, see <https://www.gnu.org/licenses/>. #
|
|
##########################################################################
|
|
"""
|
|
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.7
|
|
|
|
PyQt5
|
|
You should already have this installed, OpenLP doesn't work without it. The
|
|
version the script expects is the packaged one available from pypi.
|
|
|
|
PyEnchant
|
|
This script expects the precompiled, installable version of PyEnchant to be
|
|
installed. You can find this on the PyEnchant site.
|
|
|
|
WiX Toolset
|
|
The toolset should be installed into "C:\\%PROGRAMFILES%\\WiX Toolset v3.11"
|
|
or similar.
|
|
|
|
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 can be installed from pypi.
|
|
|
|
Git
|
|
You need the command line "git" 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".
|
|
|
|
windows-builder.py
|
|
This script, of course. It should be in the "windows-installer" directory
|
|
at the same level as OpenLP trunk.
|
|
|
|
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.
|
|
|
|
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 glob
|
|
import sys
|
|
from distutils import dir_util
|
|
from hashlib import md5
|
|
from shutil import copy, copytree, move, rmtree
|
|
|
|
from lxml.etree import ElementTree
|
|
from lxml.builder import E, ElementMaker
|
|
from lxml.objectify import fromstring
|
|
|
|
from builder import Builder
|
|
|
|
|
|
class WindowsBuilder(Builder):
|
|
"""
|
|
The :class:`WindowsBuilder` class encapsulates everything that is needed
|
|
to build a Windows installer.
|
|
"""
|
|
|
|
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 self._walk_dirs(dir_dict[search_key], os.sep.join(parts))
|
|
else:
|
|
return None
|
|
|
|
def _get_dirs_and_files(self, install_dir, start_dir):
|
|
"""
|
|
Walk down a directory recursively and build up the XML for WiX
|
|
"""
|
|
self._openlp_id = None
|
|
start_base, start_path = os.path.split(start_dir)
|
|
element = install_dir
|
|
directories = {start_path: {'__dir__': element}}
|
|
components = []
|
|
component_ids = []
|
|
FxE = ElementMaker(namespace='http://schemas.microsoft.com/wix/FirewallExtension',
|
|
nsmap={'fw': 'http://schemas.microsoft.com/wix/FirewallExtension'})
|
|
|
|
for root, _, files in os.walk(start_dir):
|
|
parent = os.sep.join(root.replace(os.path.join(start_base, ''), '').split(os.sep)[:-1])
|
|
base = os.path.basename(root)
|
|
if root != start_dir:
|
|
dir_id = 'd_{}'.format(md5(os.path.join(parent, base).encode('utf8')).hexdigest())
|
|
new_element = E.Directory(Id=dir_id, Name=base)
|
|
element.append(new_element)
|
|
element = new_element
|
|
new_dir = {'__dir__': element}
|
|
parent_dir = self._walk_dirs(directories, parent)
|
|
parent_dir[base] = new_dir
|
|
parent_dir['__dir__'].append(element)
|
|
for fname in files:
|
|
source = os.path.join(root, fname)
|
|
source_id = 'f_{}'.format(md5(source.encode('utf8')).hexdigest())
|
|
component_ids.append(source_id)
|
|
if self.arch == 'x64':
|
|
file_ = E.File(Id=source_id, Name=fname, Source=source, ProcessorArchitecture='x64')
|
|
component = E.Component(file_, Id=source_id, Guid='*', DiskId='1', Win64='yes')
|
|
else:
|
|
file_ = E.File(Id=source_id, Name=fname, Source=source)
|
|
component = E.Component(file_, Id=source_id, Guid='*', DiskId='1')
|
|
if source.endswith('OpenLP.exe'):
|
|
self._openlp_id = source_id
|
|
file_.set('KeyPath', 'yes')
|
|
fw_program = '[#{}]'.format(source_id)
|
|
component.append(FxE.FirewallException(Id='OpenLP_TCP', Name='$(var.ProductName)',
|
|
IgnoreFailure='yes', Program=fw_program,
|
|
Protocol='tcp', Scope='any'))
|
|
component.append(FxE.FirewallException(Id='OpenLP_UDP', Name='$(var.ProductName)',
|
|
IgnoreFailure='yes', Program=fw_program,
|
|
Protocol='udp', Scope='any'))
|
|
component.append(E.Shortcut(Id='ApplicationStartMenuShortcut', Name='$(var.ProductName)',
|
|
Description='$(var.Description)', Directory='ProgramMenuDir',
|
|
Icon='OpenLP.ico', Advertise='yes', WorkingDirectory='INSTALLDIR'))
|
|
component.append(E.Shortcut(Id='DebugStartMenuShortcut', Name='$(var.ProductName) (Debug)',
|
|
Description='Run $(var.ProductName) with debug logging enabled',
|
|
Directory='ProgramMenuDir', Arguments='--log-level debug',
|
|
Icon='OpenLP.ico', Advertise='yes', WorkingDirectory='INSTALLDIR'))
|
|
component.append(E.ProgId(
|
|
E.Extension(
|
|
E.Verb(Id="Open", Command="Open", Argument=" "%1""),
|
|
E.MIME(Advertise="yes", ContentType="application/-x-openlp-service", Default="yes"),
|
|
Id="osz"
|
|
),
|
|
E.Extension(
|
|
E.Verb(Id="Open", Command="Open", Argument=" "%1""),
|
|
E.MIME(Advertise="yes", ContentType="application/-x-openlp-service-lite", Default="yes"),
|
|
Id="oszl"
|
|
),
|
|
Id="OpenLP.Service",
|
|
Description="OpenLP Service File",
|
|
Icon="service_file.ico",
|
|
Advertise="yes"
|
|
))
|
|
elif source.endswith('OpenLP.chm'):
|
|
component.append(E.Shortcut(Id='HelpStartMenuShortcut', Name='$(var.ProductName) Help',
|
|
Description='Help file for $(var.ProductName)',
|
|
Target='[#{}]'.format(source_id), WorkingDirectory='INSTALLDIR'))
|
|
element.append(component)
|
|
components.append(component)
|
|
return component_ids
|
|
|
|
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'), 'rt') as base_file:
|
|
xml = base_file.read()
|
|
# convert the version string to format x.x.x if needed
|
|
if '.dev' in self.version:
|
|
windows_version = self.version.replace('.dev', '.')
|
|
windows_version = windows_version.rsplit('+', 1)[0]
|
|
else:
|
|
windows_version = self.version
|
|
xml = xml % {
|
|
'dialog': os.path.join(config_dir, 'WizardMain.bmp'),
|
|
'banner': os.path.join(config_dir, 'WizardBanner.bmp'),
|
|
'license': os.path.join(config_dir, 'LICENSE.rtf'),
|
|
'platform': self.arch,
|
|
'progfilefolder': 'ProgramFiles64Folder' if self.arch == 'x64' else 'ProgramFilesFolder',
|
|
'systemfolder': 'System64Folder' if self.arch == 'x64' else 'SystemFolder',
|
|
'version': windows_version
|
|
}
|
|
root = fromstring(xml.encode('utf8'))
|
|
# Find the INSTALLDIR directory component and populate it with our files and folders
|
|
install_dir = root.xpath('//wix:Directory[@Id="INSTALLDIR"]',
|
|
namespaces={'wix': 'http://schemas.microsoft.com/wix/2006/wi'})[0]
|
|
self._print_verbose('Creating XML fragments from files and directories')
|
|
component_ids = self._get_dirs_and_files(install_dir, self.dist_path)
|
|
# Write the property for the "Run OpenLP" checkbox
|
|
product = root.xpath('//wix:Product',
|
|
namespaces={'wix': 'http://schemas.microsoft.com/wix/2006/wi'})[0]
|
|
product.append(E.Property(Id='WixShellExecTarget', Value='[#{}]'.format(self._openlp_id)))
|
|
# Set the component ids for the feature
|
|
feature = root.xpath('//wix:Feature',
|
|
namespaces={'wix': 'http://schemas.microsoft.com/wix/2006/wi'})[0]
|
|
for component_id in component_ids:
|
|
feature.append(E.ComponentRef(Id=component_id))
|
|
self._print_verbose('Writing new WiX file')
|
|
tree = ElementTree(root)
|
|
with open(os.path.join(config_dir, 'OpenLP.wxs'), 'wb') as f:
|
|
tree.write(f, 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...')
|
|
if self.arch == 'x64':
|
|
version = '{}-x64'.format(self.version)
|
|
else:
|
|
version = self.version
|
|
msi_file = os.path.abspath(os.path.join(self.dist_path, '..', 'OpenLP-{}.msi'.format(version)))
|
|
if os.path.exists(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', '-ext', 'WixUIExtension',
|
|
'-ext', 'WixFirewallExtension', os.path.join(config_dir, 'OpenLP.wxs')],
|
|
'Error running WiX tool: candle')
|
|
self._run_command([self.light_exe, '-ext', 'WiXUtilExtension', '-ext', 'WixUIExtension',
|
|
'-ext', 'WixFirewallExtension', '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 '.dev' in self.version:
|
|
version, revision = self.version.split('.dev')
|
|
version = version + '.0' * (2 - version.count('.'))
|
|
self.portable_version = version + '.' + revision.split('+')[0]
|
|
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)
|
|
content = content.replace('%(arch)s', self.arch)
|
|
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_{ver}-{arch}.paf.exe'.format(ver=self.portable_version, arch=self.arch)
|
|
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,
|
|
'progfilesx86': self.program_files_x86,
|
|
'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', 'Qt', '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')
|
|
self.program_files_x86 = os.getenv('PROGRAMFILES(x86)')
|
|
self._print_verbose(' {:.<20}: {}'.format('site packages: ', self.site_packages))
|
|
self._print_verbose(' {:.<20}: {}'.format('program files: ', self.program_files))
|
|
self._print_verbose(' {:.<20}: {}'.format('program files x86: ', self.program_files_x86))
|
|
|
|
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')
|
|
|
|
def setup_extra(self):
|
|
"""
|
|
Extra setup to run
|
|
"""
|
|
# Detect python instance bit size
|
|
self.arch = 'x86' if sys.maxsize == 0x7fffffff else 'x64'
|
|
|
|
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('... service_file.ico')
|
|
config_dir = os.path.dirname(self.config_path)
|
|
copy(os.path.join(config_dir, 'service_file.ico'), os.path.join(self.dist_path, 'service_file.ico'))
|
|
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')
|
|
try:
|
|
# Check if PyMuPDF is installed
|
|
import fitz # noqa
|
|
HAS_PYMUPDF = True
|
|
except ImportError:
|
|
HAS_PYMUPDF = False
|
|
if not HAS_PYMUPDF:
|
|
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')
|
|
vlc_path = os.path.join(self.program_files, 'VideoLAN', 'VLC')
|
|
vlc_dest = os.path.join(self.dist_path, 'vlc')
|
|
if not os.path.exists(vlc_dest):
|
|
os.makedirs(vlc_dest)
|
|
for fname in ['libvlc.dll', 'libvlccore.dll']:
|
|
self._print_verbose('... {}'.format(fname))
|
|
copy(os.path.join(vlc_path, fname), os.path.join(vlc_dest, fname))
|
|
if os.path.exists(os.path.join(vlc_dest, 'plugins')):
|
|
rmtree(os.path.join(vlc_dest, 'plugins'))
|
|
self._print_verbose('... copying VLC plugins')
|
|
copytree(os.path.join(vlc_path, 'plugins'), os.path.join(vlc_dest, 'plugins'))
|
|
|
|
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._create_wix_file()
|
|
self._run_wix_tools()
|
|
if self.args.portable:
|
|
self._run_portableapp_builder()
|
|
|
|
def get_extra_parameters(self):
|
|
"""
|
|
Return a list of any extra parameters we wish to use
|
|
"""
|
|
parameters = []
|
|
dll_path = '{pf}\\Windows Kits\\10\\Redist\\ucrt\\DLLs\\{arch}\\*.dll'.format(pf=self.program_files_x86,
|
|
arch=self.arch)
|
|
# Finds the UCRT DDLs available from the Windows 10 SDK
|
|
for binary in glob.glob(dll_path):
|
|
parameters.append('--add-binary')
|
|
parameters.append(binary + ";.")
|
|
return parameters
|
|
|
|
|
|
if __name__ == '__main__':
|
|
WindowsBuilder().main()
|