wix.py
327 lines
| 10.3 KiB
| text/x-python
|
PythonLexer
Gregory Szorc
|
r42118 | # wix.py - WiX installer functionality | ||
# | ||||
# Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
# no-check-code because Python 3 native. | ||||
import os | ||||
import pathlib | ||||
import re | ||||
import subprocess | ||||
Gregory Szorc
|
r42123 | import tempfile | ||
Augie Fackler
|
r42215 | import typing | ||
Gregory Szorc
|
r42123 | import xml.dom.minidom | ||
Gregory Szorc
|
r42118 | |||
from .downloads import ( | ||||
download_entry, | ||||
) | ||||
from .py2exe import ( | ||||
build_py2exe, | ||||
) | ||||
from .util import ( | ||||
extract_zip_to_directory, | ||||
sign_with_signtool, | ||||
) | ||||
SUPPORT_WXS = [ | ||||
('contrib.wxs', r'contrib'), | ||||
('dist.wxs', r'dist'), | ||||
('doc.wxs', r'doc'), | ||||
('help.wxs', r'mercurial\help'), | ||||
('i18n.wxs', r'i18n'), | ||||
('locale.wxs', r'mercurial\locale'), | ||||
('templates.wxs', r'mercurial\templates'), | ||||
] | ||||
EXTRA_PACKAGES = { | ||||
'distutils', | ||||
'pygments', | ||||
} | ||||
def find_version(source_dir: pathlib.Path): | ||||
version_py = source_dir / 'mercurial' / '__version__.py' | ||||
with version_py.open('r', encoding='utf-8') as fh: | ||||
source = fh.read().strip() | ||||
m = re.search('version = b"(.*)"', source) | ||||
return m.group(1) | ||||
def normalize_version(version): | ||||
"""Normalize Mercurial version string so WiX accepts it. | ||||
Version strings have to be numeric X.Y.Z. | ||||
""" | ||||
if '+' in version: | ||||
version, extra = version.split('+', 1) | ||||
else: | ||||
extra = None | ||||
# 4.9rc0 | ||||
if version[:-1].endswith('rc'): | ||||
version = version[:-3] | ||||
versions = [int(v) for v in version.split('.')] | ||||
while len(versions) < 3: | ||||
versions.append(0) | ||||
major, minor, build = versions[:3] | ||||
if extra: | ||||
# <commit count>-<hash>+<date> | ||||
build = int(extra.split('-')[0]) | ||||
return '.'.join('%d' % x for x in (major, minor, build)) | ||||
def ensure_vc90_merge_modules(build_dir): | ||||
x86 = ( | ||||
download_entry('vc9-crt-x86-msm', build_dir, | ||||
local_name='microsoft.vcxx.crt.x86_msm.msm')[0], | ||||
download_entry('vc9-crt-x86-msm-policy', build_dir, | ||||
local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm')[0] | ||||
) | ||||
x64 = ( | ||||
download_entry('vc9-crt-x64-msm', build_dir, | ||||
local_name='microsoft.vcxx.crt.x64_msm.msm')[0], | ||||
download_entry('vc9-crt-x64-msm-policy', build_dir, | ||||
local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm')[0] | ||||
) | ||||
return { | ||||
'x86': x86, | ||||
'x64': x64, | ||||
} | ||||
def run_candle(wix, cwd, wxs, source_dir, defines=None): | ||||
args = [ | ||||
str(wix / 'candle.exe'), | ||||
'-nologo', | ||||
str(wxs), | ||||
'-dSourceDir=%s' % source_dir, | ||||
] | ||||
if defines: | ||||
args.extend('-d%s=%s' % define for define in sorted(defines.items())) | ||||
subprocess.run(args, cwd=str(cwd), check=True) | ||||
def make_post_build_signing_fn(name, subject_name=None, cert_path=None, | ||||
cert_password=None, timestamp_url=None): | ||||
"""Create a callable that will use signtool to sign hg.exe.""" | ||||
def post_build_sign(source_dir, build_dir, dist_dir, version): | ||||
description = '%s %s' % (name, version) | ||||
sign_with_signtool(dist_dir / 'hg.exe', description, | ||||
subject_name=subject_name, cert_path=cert_path, | ||||
cert_password=cert_password, | ||||
timestamp_url=timestamp_url) | ||||
return post_build_sign | ||||
Gregory Szorc
|
r42123 | LIBRARIES_XML = ''' | ||
<?xml version="1.0" encoding="utf-8"?> | ||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"> | ||||
<?include {wix_dir}/guids.wxi ?> | ||||
<?include {wix_dir}/defines.wxi ?> | ||||
<Fragment> | ||||
<DirectoryRef Id="INSTALLDIR" FileSource="$(var.SourceDir)"> | ||||
<Directory Id="libdir" Name="lib" FileSource="$(var.SourceDir)/lib"> | ||||
<Component Id="libOutput" Guid="$(var.lib.guid)" Win64='$(var.IsX64)'> | ||||
</Component> | ||||
</Directory> | ||||
</DirectoryRef> | ||||
</Fragment> | ||||
</Wix> | ||||
'''.lstrip() | ||||
def make_libraries_xml(wix_dir: pathlib.Path, dist_dir: pathlib.Path): | ||||
"""Make XML data for library components WXS.""" | ||||
# We can't use ElementTree because it doesn't handle the | ||||
# <?include ?> directives. | ||||
doc = xml.dom.minidom.parseString( | ||||
LIBRARIES_XML.format(wix_dir=str(wix_dir))) | ||||
component = doc.getElementsByTagName('Component')[0] | ||||
f = doc.createElement('File') | ||||
f.setAttribute('Name', 'library.zip') | ||||
f.setAttribute('KeyPath', 'yes') | ||||
component.appendChild(f) | ||||
lib_dir = dist_dir / 'lib' | ||||
for p in sorted(lib_dir.iterdir()): | ||||
if not p.name.endswith(('.dll', '.pyd')): | ||||
continue | ||||
f = doc.createElement('File') | ||||
f.setAttribute('Name', p.name) | ||||
component.appendChild(f) | ||||
return doc.toprettyxml() | ||||
Gregory Szorc
|
r42118 | def build_installer(source_dir: pathlib.Path, python_exe: pathlib.Path, | ||
Augie Fackler
|
r42214 | msi_name='mercurial', version=None, post_build_fn=None, | ||
Augie Fackler
|
r42216 | extra_packages_script=None, | ||
extra_wxs:typing.Optional[typing.Dict[str,str]]=None, | ||||
extra_features:typing.Optional[typing.List[str]]=None): | ||||
Gregory Szorc
|
r42118 | """Build a WiX MSI installer. | ||
``source_dir`` is the path to the Mercurial source tree to use. | ||||
``arch`` is the target architecture. either ``x86`` or ``x64``. | ||||
``python_exe`` is the path to the Python executable to use/bundle. | ||||
``version`` is the Mercurial version string. If not defined, | ||||
``mercurial/__version__.py`` will be consulted. | ||||
``post_build_fn`` is a callable that will be called after building | ||||
Mercurial but before invoking WiX. It can be used to e.g. facilitate | ||||
signing. It is passed the paths to the Mercurial source, build, and | ||||
dist directories and the resolved Mercurial version. | ||||
Augie Fackler
|
r42214 | ``extra_packages_script`` is a command to be run to inject extra packages | ||
into the py2exe binary. It should stage packages into the virtualenv and | ||||
print a null byte followed by a newline-separated list of packages that | ||||
should be included in the exe. | ||||
Augie Fackler
|
r42215 | ``extra_wxs`` is a dict of {wxs_name: working_dir_for_wxs_build}. | ||
Augie Fackler
|
r42216 | ``extra_features`` is a list of additional named Features to include in | ||
the build. These must match Feature names in one of the wxs scripts. | ||||
Gregory Szorc
|
r42118 | """ | ||
arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86' | ||||
hg_build_dir = source_dir / 'build' | ||||
dist_dir = source_dir / 'dist' | ||||
Gregory Szorc
|
r42122 | wix_dir = source_dir / 'contrib' / 'packaging' / 'wix' | ||
Gregory Szorc
|
r42118 | |||
Gregory Szorc
|
r42122 | requirements_txt = wix_dir / 'requirements.txt' | ||
Gregory Szorc
|
r42118 | |||
build_py2exe(source_dir, hg_build_dir, | ||||
python_exe, 'wix', requirements_txt, | ||||
Augie Fackler
|
r42214 | extra_packages=EXTRA_PACKAGES, | ||
extra_packages_script=extra_packages_script) | ||||
Gregory Szorc
|
r42118 | |||
version = version or normalize_version(find_version(source_dir)) | ||||
print('using version string: %s' % version) | ||||
if post_build_fn: | ||||
post_build_fn(source_dir, hg_build_dir, dist_dir, version) | ||||
build_dir = hg_build_dir / ('wix-%s' % arch) | ||||
build_dir.mkdir(exist_ok=True) | ||||
wix_pkg, wix_entry = download_entry('wix', hg_build_dir) | ||||
wix_path = hg_build_dir / ('wix-%s' % wix_entry['version']) | ||||
if not wix_path.exists(): | ||||
extract_zip_to_directory(wix_pkg, wix_path) | ||||
ensure_vc90_merge_modules(hg_build_dir) | ||||
source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir)) | ||||
defines = {'Platform': arch} | ||||
for wxs, rel_path in SUPPORT_WXS: | ||||
Gregory Szorc
|
r42122 | wxs = wix_dir / wxs | ||
Gregory Szorc
|
r42118 | wxs_source_dir = source_dir / rel_path | ||
run_candle(wix_path, build_dir, wxs, wxs_source_dir, defines=defines) | ||||
Augie Fackler
|
r42215 | for source, rel_path in sorted((extra_wxs or {}).items()): | ||
run_candle(wix_path, build_dir, source, rel_path, defines=defines) | ||||
Gregory Szorc
|
r42123 | # candle.exe doesn't like when we have an open handle on the file. | ||
# So use TemporaryDirectory() instead of NamedTemporaryFile(). | ||||
with tempfile.TemporaryDirectory() as td: | ||||
td = pathlib.Path(td) | ||||
tf = td / 'library.wxs' | ||||
with tf.open('w') as fh: | ||||
fh.write(make_libraries_xml(wix_dir, dist_dir)) | ||||
run_candle(wix_path, build_dir, tf, dist_dir, defines=defines) | ||||
Gregory Szorc
|
r42122 | source = wix_dir / 'mercurial.wxs' | ||
Gregory Szorc
|
r42118 | defines['Version'] = version | ||
defines['Comments'] = 'Installs Mercurial version %s' % version | ||||
defines['VCRedistSrcDir'] = str(hg_build_dir) | ||||
Augie Fackler
|
r42216 | if extra_features: | ||
assert all(';' not in f for f in extra_features) | ||||
defines['MercurialExtraFeatures'] = ';'.join(extra_features) | ||||
Gregory Szorc
|
r42118 | |||
run_candle(wix_path, build_dir, source, source_build_rel, defines=defines) | ||||
msi_path = source_dir / 'dist' / ( | ||||
'%s-%s-%s.msi' % (msi_name, version, arch)) | ||||
args = [ | ||||
str(wix_path / 'light.exe'), | ||||
'-nologo', | ||||
'-ext', 'WixUIExtension', | ||||
'-sw1076', | ||||
'-spdb', | ||||
'-o', str(msi_path), | ||||
] | ||||
for source, rel_path in SUPPORT_WXS: | ||||
assert source.endswith('.wxs') | ||||
args.append(str(build_dir / ('%s.wixobj' % source[:-4]))) | ||||
Augie Fackler
|
r42215 | for source, rel_path in sorted((extra_wxs or {}).items()): | ||
assert source.endswith('.wxs') | ||||
source = os.path.basename(source) | ||||
args.append(str(build_dir / ('%s.wixobj' % source[:-4]))) | ||||
Gregory Szorc
|
r42123 | args.extend([ | ||
str(build_dir / 'library.wixobj'), | ||||
str(build_dir / 'mercurial.wixobj'), | ||||
]) | ||||
Gregory Szorc
|
r42118 | |||
subprocess.run(args, cwd=str(source_dir), check=True) | ||||
print('%s created' % msi_path) | ||||
return { | ||||
'msi_path': msi_path, | ||||
} | ||||
def build_signed_installer(source_dir: pathlib.Path, python_exe: pathlib.Path, | ||||
name: str, version=None, subject_name=None, | ||||
cert_path=None, cert_password=None, | ||||
Augie Fackler
|
r42215 | timestamp_url=None, extra_packages_script=None, | ||
Augie Fackler
|
r42216 | extra_wxs=None, extra_features=None): | ||
Gregory Szorc
|
r42118 | """Build an installer with signed executables.""" | ||
post_build_fn = make_post_build_signing_fn( | ||||
name, | ||||
subject_name=subject_name, | ||||
cert_path=cert_path, | ||||
cert_password=cert_password, | ||||
timestamp_url=timestamp_url) | ||||
info = build_installer(source_dir, python_exe=python_exe, | ||||
msi_name=name.lower(), version=version, | ||||
Augie Fackler
|
r42214 | post_build_fn=post_build_fn, | ||
Augie Fackler
|
r42215 | extra_packages_script=extra_packages_script, | ||
Augie Fackler
|
r42216 | extra_wxs=extra_wxs, extra_features=extra_features) | ||
Gregory Szorc
|
r42118 | |||
description = '%s %s' % (name, version) | ||||
sign_with_signtool(info['msi_path'], description, | ||||
subject_name=subject_name, cert_path=cert_path, | ||||
cert_password=cert_password, timestamp_url=timestamp_url) | ||||