mirror of
https://gitlab.com/openlp/packaging.git
synced 2024-10-31 16:54:46 +00:00
411 lines
18 KiB
Python
411 lines
18 KiB
Python
# -*- coding: utf-8 -*-
|
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
|
|
|
###############################################################################
|
|
# OpenLP - Open Source Lyrics Projection #
|
|
# --------------------------------------------------------------------------- #
|
|
# Copyright (c) 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.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.
|
|
|
|
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".
|
|
|
|
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, move, rmtree
|
|
|
|
from lxml.etree import fromstring, tostring
|
|
from lxml.builder import E
|
|
|
|
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_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 = self._walk_dirs(directories, parent)
|
|
parent_dir[base] = new_dir
|
|
parent_dir['__dir__'].append(element)
|
|
for fname in files:
|
|
source = os.path.join(path, fname) if path else fname
|
|
source_id = md5(source.encode('utf8')).hexdigest()
|
|
file_id = 'file_{source_id}'.format(source_id=source_id)
|
|
component_id = 'cmp_{source_id}'.format(source_id=source_id)
|
|
file_ = E.File(Id=file_id, KeyPath="yes", Source=source)
|
|
component = E.Component(file_, Id=component_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'), 'rt') as base_file:
|
|
xml = base_file.read()
|
|
progfilefolder = 'ProgramFiles64Folder' if self.arch == 'x64' else 'ProgramFilesFolder'
|
|
xml = xml % dict(dialog=os.path.join(config_dir, 'WizardMain.bmp'),
|
|
banner=os.path.join(config_dir, 'WizardBanner.bmp'),
|
|
license=os.path.join(config_dir, 'gpl-2.0.rtf'),
|
|
platform=self.arch,
|
|
progfilefolder=progfilefolder)
|
|
tree = fromstring(xml.encode('utf8'))
|
|
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')
|
|
for fragment in fragments:
|
|
tree.append(fragment)
|
|
self._print_verbose('Writing new WiX file')
|
|
with open(os.path.join(config_dir, '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 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', 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)
|
|
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,
|
|
'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')
|
|
# Default program_files to 'Program Files (x86)' - the folder for 32-bit programs on 64-bit systems, if that
|
|
# does not exists the host system is 32-bit so fallback to 'Program Files'.
|
|
self.program_files = os.getenv('PROGRAMFILES(x86)')
|
|
if not self.program_files:
|
|
self.program_files = os.getenv('PROGRAMFILES')
|
|
self._print_verbose(' {:.<20}: {}'.format('site packages: ', self.site_packages))
|
|
self._print_verbose(' {:.<20}: {}'.format('program files: ', self.program_files))
|
|
|
|
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'))
|
|
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')
|
|
|
|
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,
|
|
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()
|