--- a/contrib/packaging/hgpackaging/wix.py Sat Feb 19 18:42:12 2022 -0700
+++ b/contrib/packaging/hgpackaging/wix.py Sat Feb 19 22:13:11 2022 -0700
@@ -7,376 +7,16 @@
# no-check-code because Python 3 native.
-import collections
import json
import os
import pathlib
-import re
import shutil
-import subprocess
import typing
-import uuid
-import xml.dom.minidom
-from .downloads import download_entry
-from .py2exe import (
- build_py2exe,
- stage_install,
-)
from .pyoxidizer import (
build_docs_html,
- create_pyoxidizer_install_layout,
run_pyoxidizer,
)
-from .util import (
- extract_zip_to_directory,
- normalize_windows_version,
- process_install_rules,
- sign_with_signtool,
-)
-
-
-EXTRA_PACKAGES = {
- 'dulwich',
- 'distutils',
- 'keyring',
- 'pygments',
- 'win32ctypes',
-}
-
-EXTRA_INCLUDES = {
- '_curses',
- '_curses_panel',
-}
-
-EXTRA_INSTALL_RULES = [
- ('contrib/packaging/wix/COPYING.rtf', 'COPYING.rtf'),
- ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
-]
-
-STAGING_REMOVE_FILES = [
- # We use the RTF variant.
- 'copying.txt',
-]
-
-SHORTCUTS = {
- # hg.1.html'
- 'hg.file.5d3e441c_28d9_5542_afd0_cdd4234f12d5': {
- 'Name': 'Mercurial Command Reference',
- },
- # hgignore.5.html
- 'hg.file.5757d8e0_f207_5e10_a2ec_3ba0a062f431': {
- 'Name': 'Mercurial Ignore Files',
- },
- # hgrc.5.html
- 'hg.file.92e605fd_1d1a_5dc6_9fc0_5d2998eb8f5e': {
- 'Name': 'Mercurial Configuration Files',
- },
-}
-
-
-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 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_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
- """Create XML string listing every file to be installed."""
-
- # We derive GUIDs from a deterministic file path identifier.
- # We shoehorn the name into something that looks like a URL because
- # the UUID namespaces are supposed to work that way (even though
- # the input data probably is never validated).
-
- doc = xml.dom.minidom.parseString(
- '<?xml version="1.0" encoding="utf-8"?>'
- '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">'
- '</Wix>'
- )
-
- # Assemble the install layout by directory. This makes it easier to
- # emit XML, since each directory has separate entities.
- manifest = collections.defaultdict(dict)
-
- for root, dirs, files in os.walk(staging_dir):
- dirs.sort()
-
- root = pathlib.Path(root)
- rel_dir = root.relative_to(staging_dir)
-
- for i in range(len(rel_dir.parts)):
- parent = '/'.join(rel_dir.parts[0 : i + 1])
- manifest.setdefault(parent, {})
-
- for f in sorted(files):
- full = root / f
- manifest[str(rel_dir).replace('\\', '/')][full.name] = full
-
- component_groups = collections.defaultdict(list)
-
- # Now emit a <Fragment> for each directory.
- # Each directory is composed of a <DirectoryRef> pointing to its parent
- # and defines child <Directory>'s and a <Component> with all the files.
- for dir_name, entries in sorted(manifest.items()):
- # The directory id is derived from the path. But the root directory
- # is special.
- if dir_name == '.':
- parent_directory_id = 'INSTALLDIR'
- else:
- parent_directory_id = 'hg.dir.%s' % dir_name.replace(
- '/', '.'
- ).replace('-', '_')
-
- fragment = doc.createElement('Fragment')
- directory_ref = doc.createElement('DirectoryRef')
- directory_ref.setAttribute('Id', parent_directory_id)
-
- # Add <Directory> entries for immediate children directories.
- for possible_child in sorted(manifest.keys()):
- if (
- dir_name == '.'
- and '/' not in possible_child
- and possible_child != '.'
- ):
- child_directory_id = ('hg.dir.%s' % possible_child).replace(
- '-', '_'
- )
- name = possible_child
- else:
- if not possible_child.startswith('%s/' % dir_name):
- continue
- name = possible_child[len(dir_name) + 1 :]
- if '/' in name:
- continue
-
- child_directory_id = 'hg.dir.%s' % possible_child.replace(
- '/', '.'
- ).replace('-', '_')
-
- directory = doc.createElement('Directory')
- directory.setAttribute('Id', child_directory_id)
- directory.setAttribute('Name', name)
- directory_ref.appendChild(directory)
-
- # Add <Component>s for files in this directory.
- for rel, source_path in sorted(entries.items()):
- if dir_name == '.':
- full_rel = rel
- else:
- full_rel = '%s/%s' % (dir_name, rel)
-
- component_unique_id = (
- 'https://www.mercurial-scm.org/wix-installer/0/component/%s'
- % full_rel
- )
- component_guid = uuid.uuid5(uuid.NAMESPACE_URL, component_unique_id)
- component_id = 'hg.component.%s' % str(component_guid).replace(
- '-', '_'
- )
-
- component = doc.createElement('Component')
-
- component.setAttribute('Id', component_id)
- component.setAttribute('Guid', str(component_guid).upper())
- component.setAttribute('Win64', 'yes' if is_x64 else 'no')
-
- # Assign this component to a top-level group.
- if dir_name == '.':
- component_groups['ROOT'].append(component_id)
- elif '/' in dir_name:
- component_groups[dir_name[0 : dir_name.index('/')]].append(
- component_id
- )
- else:
- component_groups[dir_name].append(component_id)
-
- unique_id = (
- 'https://www.mercurial-scm.org/wix-installer/0/%s' % full_rel
- )
- file_guid = uuid.uuid5(uuid.NAMESPACE_URL, unique_id)
-
- # IDs have length limits. So use GUID to derive them.
- file_guid_normalized = str(file_guid).replace('-', '_')
- file_id = 'hg.file.%s' % file_guid_normalized
-
- file_element = doc.createElement('File')
- file_element.setAttribute('Id', file_id)
- file_element.setAttribute('Source', str(source_path))
- file_element.setAttribute('KeyPath', 'yes')
- file_element.setAttribute('ReadOnly', 'yes')
-
- component.appendChild(file_element)
- directory_ref.appendChild(component)
-
- fragment.appendChild(directory_ref)
- doc.documentElement.appendChild(fragment)
-
- for group, component_ids in sorted(component_groups.items()):
- fragment = doc.createElement('Fragment')
- component_group = doc.createElement('ComponentGroup')
- component_group.setAttribute('Id', 'hg.group.%s' % group)
-
- for component_id in component_ids:
- component_ref = doc.createElement('ComponentRef')
- component_ref.setAttribute('Id', component_id)
- component_group.appendChild(component_ref)
-
- fragment.appendChild(component_group)
- doc.documentElement.appendChild(fragment)
-
- # Add <Shortcut> to files that have it defined.
- for file_id, metadata in sorted(SHORTCUTS.items()):
- els = doc.getElementsByTagName('File')
- els = [el for el in els if el.getAttribute('Id') == file_id]
-
- if not els:
- raise Exception('could not find File[Id=%s]' % file_id)
-
- for el in els:
- shortcut = doc.createElement('Shortcut')
- shortcut.setAttribute('Id', 'hg.shortcut.%s' % file_id)
- shortcut.setAttribute('Directory', 'ProgramMenuDir')
- shortcut.setAttribute('Icon', 'hgIcon.ico')
- shortcut.setAttribute('IconIndex', '0')
- shortcut.setAttribute('Advertise', 'yes')
- for k, v in sorted(metadata.items()):
- shortcut.setAttribute(k, v)
-
- el.appendChild(shortcut)
-
- return doc.toprettyxml()
-
-
-def build_installer_py2exe(
- source_dir: pathlib.Path,
- python_exe: pathlib.Path,
- msi_name='mercurial',
- version=None,
- extra_packages_script=None,
- extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
- extra_features: typing.Optional[typing.List[str]] = None,
- signing_info: typing.Optional[typing.Dict[str, str]] = None,
-):
- """Build a WiX MSI installer using py2exe.
-
- ``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.
- ``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'
-
- requirements_txt = (
- source_dir / 'contrib' / 'packaging' / 'requirements-windows-py2.txt'
- )
-
- build_py2exe(
- source_dir,
- hg_build_dir,
- python_exe,
- 'wix',
- requirements_txt,
- extra_packages=EXTRA_PACKAGES,
- extra_packages_script=extra_packages_script,
- extra_includes=EXTRA_INCLUDES,
- )
-
- build_dir = hg_build_dir / ('wix-%s' % arch)
- staging_dir = build_dir / 'stage'
-
- build_dir.mkdir(exist_ok=True)
-
- # Purge the staging directory for every build so packaging is pristine.
- if staging_dir.exists():
- print('purging %s' % staging_dir)
- shutil.rmtree(staging_dir)
-
- stage_install(source_dir, staging_dir, lower_case=True)
-
- # We also install some extra files.
- process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
-
- # And remove some files we don't want.
- for f in STAGING_REMOVE_FILES:
- p = staging_dir / f
- if p.exists():
- print('removing %s' % p)
- p.unlink()
-
- return run_wix_packaging(
- source_dir,
- build_dir,
- staging_dir,
- arch,
- version=version,
- python2=True,
- msi_name=msi_name,
- suffix="-python2",
- extra_wxs=extra_wxs,
- extra_features=extra_features,
- signing_info=signing_info,
- )
def build_installer_pyoxidizer(
@@ -454,133 +94,3 @@
return {
"msi_path": dist_path,
}
-
-
-def run_wix_packaging(
- source_dir: pathlib.Path,
- build_dir: pathlib.Path,
- staging_dir: pathlib.Path,
- arch: str,
- version: str,
- python2: bool,
- msi_name: typing.Optional[str] = "mercurial",
- suffix: str = "",
- extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
- extra_features: typing.Optional[typing.List[str]] = None,
- signing_info: typing.Optional[typing.Dict[str, str]] = None,
-):
- """Invokes WiX to package up a built Mercurial.
-
- ``signing_info`` is a dict defining properties to facilitate signing the
- installer. Recognized keys include ``name``, ``subject_name``,
- ``cert_path``, ``cert_password``, and ``timestamp_url``. If populated,
- we will sign both the hg.exe and the .msi using the signing credentials
- specified.
- """
-
- orig_version = version or find_version(source_dir)
- version = normalize_windows_version(orig_version)
- print('using version string: %s' % version)
- if version != orig_version:
- print('(normalized from: %s)' % orig_version)
-
- if signing_info:
- sign_with_signtool(
- staging_dir / "hg.exe",
- "%s %s" % (signing_info["name"], version),
- subject_name=signing_info["subject_name"],
- cert_path=signing_info["cert_path"],
- cert_password=signing_info["cert_password"],
- timestamp_url=signing_info["timestamp_url"],
- )
-
- wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
-
- wix_pkg, wix_entry = download_entry('wix', build_dir)
- wix_path = build_dir / ('wix-%s' % wix_entry['version'])
-
- if not wix_path.exists():
- extract_zip_to_directory(wix_pkg, wix_path)
-
- if python2:
- ensure_vc90_merge_modules(build_dir)
-
- source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
-
- defines = {'Platform': arch}
-
- # Derive a .wxs file with the staged files.
- manifest_wxs = build_dir / 'stage.wxs'
- with manifest_wxs.open('w', encoding='utf-8') as fh:
- fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
-
- run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
-
- for source, rel_path in sorted((extra_wxs or {}).items()):
- run_candle(wix_path, build_dir, source, rel_path, defines=defines)
-
- source = wix_dir / 'mercurial.wxs'
- defines['Version'] = version
- defines['Comments'] = 'Installs Mercurial version %s' % version
-
- if python2:
- defines["PythonVersion"] = "2"
- defines['VCRedistSrcDir'] = str(build_dir)
- else:
- defines["PythonVersion"] = "3"
-
- if (staging_dir / "lib").exists():
- defines["MercurialHasLib"] = "1"
-
- 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%s.msi' % (msi_name, orig_version, arch, suffix))
- )
-
- args = [
- str(wix_path / 'light.exe'),
- '-nologo',
- '-ext',
- 'WixUIExtension',
- '-sw1076',
- '-spdb',
- '-o',
- str(msi_path),
- ]
-
- 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 / 'stage.wixobj'),
- str(build_dir / 'mercurial.wixobj'),
- ]
- )
-
- subprocess.run(args, cwd=str(source_dir), check=True)
-
- print('%s created' % msi_path)
-
- if signing_info:
- sign_with_signtool(
- msi_path,
- "%s %s" % (signing_info["name"], version),
- subject_name=signing_info["subject_name"],
- cert_path=signing_info["cert_path"],
- cert_password=signing_info["cert_password"],
- timestamp_url=signing_info["timestamp_url"],
- )
-
- return {
- 'msi_path': msi_path,
- }