##// END OF EJS Templates
style: run a patched black on a subset of mercurial...
style: run a patched black on a subset of mercurial This applied black to the 20 smallest files in mercurial/: ls -S1 mercurial/*.py | tail -n20 | xargs black --skip-string-normalization Note that a few files failed to format, presumably due to a bug in my patch. The intent is to be able to compare results to D5064 with https://github.com/python/black/pull/826 applied to black. I skipped string normalization on this patch for clarity - in reality I think we'd want one pass without string normalization, followed by another to normalize strings (which is basically replacing ' with " globally.) # skip-blame mass-reformatting only Differential Revision: https://phab.mercurial-scm.org/D6342

File last commit:

r42216:1711f581 default
r43345:57875cf4 default
Show More
wix.py
327 lines | 10.3 KiB | text/x-python | PythonLexer
# 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
import tempfile
import typing
import xml.dom.minidom
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
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()
def build_installer(source_dir: pathlib.Path, python_exe: pathlib.Path,
msi_name='mercurial', version=None, post_build_fn=None,
extra_packages_script=None,
extra_wxs:typing.Optional[typing.Dict[str,str]]=None,
extra_features:typing.Optional[typing.List[str]]=None):
"""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.
``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.
``extra_wxs`` is a dict of {wxs_name: working_dir_for_wxs_build}.
``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.
"""
arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
hg_build_dir = source_dir / 'build'
dist_dir = source_dir / 'dist'
wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
requirements_txt = wix_dir / 'requirements.txt'
build_py2exe(source_dir, hg_build_dir,
python_exe, 'wix', requirements_txt,
extra_packages=EXTRA_PACKAGES,
extra_packages_script=extra_packages_script)
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:
wxs = wix_dir / wxs
wxs_source_dir = source_dir / rel_path
run_candle(wix_path, build_dir, wxs, wxs_source_dir, defines=defines)
for source, rel_path in sorted((extra_wxs or {}).items()):
run_candle(wix_path, build_dir, source, rel_path, defines=defines)
# 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)
source = wix_dir / 'mercurial.wxs'
defines['Version'] = version
defines['Comments'] = 'Installs Mercurial version %s' % version
defines['VCRedistSrcDir'] = str(hg_build_dir)
if extra_features:
assert all(';' not in f for f in extra_features)
defines['MercurialExtraFeatures'] = ';'.join(extra_features)
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])))
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])))
args.extend([
str(build_dir / 'library.wixobj'),
str(build_dir / 'mercurial.wixobj'),
])
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,
timestamp_url=None, extra_packages_script=None,
extra_wxs=None, extra_features=None):
"""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,
post_build_fn=post_build_fn,
extra_packages_script=extra_packages_script,
extra_wxs=extra_wxs, extra_features=extra_features)
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)