diff --git a/contrib/packaging/hgpackaging/py2exe.py b/contrib/packaging/hgpackaging/py2exe.py
--- a/contrib/packaging/hgpackaging/py2exe.py
+++ b/contrib/packaging/hgpackaging/py2exe.py
@@ -209,13 +209,26 @@
)
-def stage_install(source_dir: pathlib.Path, staging_dir: pathlib.Path):
+def stage_install(
+ source_dir: pathlib.Path, staging_dir: pathlib.Path, lower_case=False
+):
"""Copy all files to be installed to a directory.
This allows packaging to simply walk a directory tree to find source
files.
"""
- process_install_rules(STAGING_RULES, source_dir, staging_dir)
+ if lower_case:
+ rules = []
+ for source, dest in STAGING_RULES:
+ # Only lower directory names.
+ if '/' in dest:
+ parent, leaf = dest.rsplit('/', 1)
+ dest = '%s/%s' % (parent.lower(), leaf)
+ rules.append((source, dest))
+ else:
+ rules = STAGING_RULES
+
+ process_install_rules(rules, source_dir, staging_dir)
# Write out a default editor.rc file to configure notepad as the
# default editor.
diff --git a/contrib/packaging/hgpackaging/wix.py b/contrib/packaging/hgpackaging/wix.py
--- a/contrib/packaging/hgpackaging/wix.py
+++ b/contrib/packaging/hgpackaging/wix.py
@@ -7,38 +7,60 @@
# no-check-code because Python 3 native.
+import collections
import os
import pathlib
import re
+import shutil
import subprocess
-import tempfile
import typing
+import uuid
import xml.dom.minidom
from .downloads import download_entry
-from .py2exe import build_py2exe
+from .py2exe import (
+ build_py2exe,
+ stage_install,
+)
from .util import (
extract_zip_to_directory,
+ process_install_rules,
sign_with_signtool,
)
-SUPPORT_WXS = [
- ('contrib.wxs', r'contrib'),
- ('dist.wxs', r'dist'),
- ('doc.wxs', r'doc'),
- ('help.wxs', r'mercurial\help'),
- ('locale.wxs', r'mercurial\locale'),
- ('templates.wxs', r'mercurial\templates'),
-]
-
-
EXTRA_PACKAGES = {
'distutils',
'pygments',
}
+EXTRA_INSTALL_RULES = [
+ ('contrib/packaging/wix/COPYING.rtf', 'COPYING.rtf'),
+ ('contrib/win32/mercurial.ini', 'hgrc.d/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'
@@ -147,49 +169,165 @@
return post_build_sign
-LIBRARIES_XML = '''
-
-
-
-
-
+def make_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
+ """Create XML string listing every file to be installed."""
-
-
-
-
-
-
-
-
-
-'''.lstrip()
+ # 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).
-
-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
- # directives.
doc = xml.dom.minidom.parseString(
- LIBRARIES_XML.format(wix_dir=str(wix_dir))
+ ''
+ ''
+ ''
)
- component = doc.getElementsByTagName('Component')[0]
+ # 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 for each directory.
+ # Each directory is composed of a pointing to its parent
+ # and defines child 's and a 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('/', '.')
- f = doc.createElement('File')
- f.setAttribute('Name', 'library.zip')
- f.setAttribute('KeyPath', 'yes')
- component.appendChild(f)
+ fragment = doc.createElement('Fragment')
+ directory_ref = doc.createElement('DirectoryRef')
+ directory_ref.setAttribute('Id', parent_directory_id)
+
+ # Add 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
+ 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(
+ '/', '.'
+ )
+
+ directory = doc.createElement('Directory')
+ directory.setAttribute('Id', child_directory_id)
+ directory.setAttribute('Name', name)
+ directory_ref.appendChild(directory)
+
+ # Add 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)
- lib_dir = dist_dir / 'lib'
+ 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
- for p in sorted(lib_dir.iterdir()):
- if not p.name.endswith(('.dll', '.pyd')):
- continue
+ 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)
- f = doc.createElement('File')
- f.setAttribute('Name', p.name)
- component.appendChild(f)
+ fragment.appendChild(component_group)
+ doc.documentElement.appendChild(fragment)
+
+ # Add 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()
@@ -248,9 +386,27 @@
post_build_fn(source_dir, hg_build_dir, dist_dir, version)
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()
+
wix_pkg, wix_entry = download_entry('wix', hg_build_dir)
wix_path = hg_build_dir / ('wix-%s' % wix_entry['version'])
@@ -263,25 +419,16 @@
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)
+ # 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)
- # 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
@@ -307,20 +454,13 @@
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'),
- ]
+ [str(build_dir / 'stage.wixobj'), str(build_dir / 'mercurial.wixobj'),]
)
subprocess.run(args, cwd=str(source_dir), check=True)
diff --git a/contrib/packaging/wix/contrib.wxs b/contrib/packaging/wix/contrib.wxs
deleted file mode 100644
--- a/contrib/packaging/wix/contrib.wxs
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/contrib/packaging/wix/dist.wxs b/contrib/packaging/wix/dist.wxs
deleted file mode 100644
--- a/contrib/packaging/wix/dist.wxs
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/contrib/packaging/wix/doc.wxs b/contrib/packaging/wix/doc.wxs
deleted file mode 100644
--- a/contrib/packaging/wix/doc.wxs
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/contrib/packaging/wix/guids.wxi b/contrib/packaging/wix/guids.wxi
--- a/contrib/packaging/wix/guids.wxi
+++ b/contrib/packaging/wix/guids.wxi
@@ -4,46 +4,9 @@
and replace 'Mercurial' in this notice with the name of
your project. Component GUIDs have global namespace! -->
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/contrib/packaging/wix/help.wxs b/contrib/packaging/wix/help.wxs
deleted file mode 100644
--- a/contrib/packaging/wix/help.wxs
+++ /dev/null
@@ -1,70 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/contrib/packaging/wix/locale.wxs b/contrib/packaging/wix/locale.wxs
deleted file mode 100644
--- a/contrib/packaging/wix/locale.wxs
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/contrib/packaging/wix/mercurial.wxs b/contrib/packaging/wix/mercurial.wxs
--- a/contrib/packaging/wix/mercurial.wxs
+++ b/contrib/packaging/wix/mercurial.wxs
@@ -60,30 +60,10 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -117,15 +97,12 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
@@ -135,13 +112,13 @@
-
+
-
+
-
+
diff --git a/contrib/packaging/wix/templates.wxs b/contrib/packaging/wix/templates.wxs
deleted file mode 100644
--- a/contrib/packaging/wix/templates.wxs
+++ /dev/null
@@ -1,251 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/contrib/win32/mercurial.ini b/contrib/win32/mercurial.ini
--- a/contrib/win32/mercurial.ini
+++ b/contrib/win32/mercurial.ini
@@ -16,7 +16,7 @@
[ui]
; editor used to enter commit logs, etc. Most text editors will work.
-editor = notepad
+; editor = notepad
; show changed files and be a bit more verbose if True
; verbose = True
; colorize commands output
diff --git a/tests/test-install.t b/tests/test-install.t
--- a/tests/test-install.t
+++ b/tests/test-install.t
@@ -162,80 +162,6 @@
"fsmonitor-watchman": "false",
"fsmonitor-watchman-error": "warning: Watchman unavailable: watchman exited with code 1",
-
-#if test-repo
- $ . "$TESTDIR/helpers-testrepo.sh"
-
- $ cat >> wixxml.py << EOF
- > import os
- > import subprocess
- > import sys
- > import xml.etree.ElementTree as ET
- > from mercurial import pycompat
- >
- > # MSYS mangles the path if it expands $TESTDIR
- > testdir = os.environ['TESTDIR']
- > ns = {'wix' : 'http://schemas.microsoft.com/wix/2006/wi'}
- >
- > def directory(node, relpath):
- > '''generator of files in the xml node, rooted at relpath'''
- > dirs = node.findall('./{%(wix)s}Directory' % ns)
- >
- > for d in dirs:
- > for subfile in directory(d, relpath + d.attrib['Name'] + '/'):
- > yield subfile
- >
- > files = node.findall('./{%(wix)s}Component/{%(wix)s}File' % ns)
- >
- > for f in files:
- > yield pycompat.sysbytes(relpath + f.attrib['Name'])
- >
- > def hgdirectory(relpath):
- > '''generator of tracked files, rooted at relpath'''
- > hgdir = "%s/../mercurial" % (testdir)
- > args = ['hg', '--cwd', hgdir, 'files', relpath]
- > proc = subprocess.Popen(args, stdout=subprocess.PIPE,
- > stderr=subprocess.PIPE)
- > output = proc.communicate()[0]
- >
- > for line in output.splitlines():
- > if os.name == 'nt':
- > yield line.replace(pycompat.sysbytes(os.sep), b'/')
- > else:
- > yield line
- >
- > tracked = [f for f in hgdirectory(sys.argv[1])]
- >
- > xml = ET.parse("%s/../contrib/packaging/wix/%s.wxs" % (testdir, sys.argv[1]))
- > root = xml.getroot()
- > dir = root.find('.//{%(wix)s}DirectoryRef' % ns)
- >
- > installed = [f for f in directory(dir, '')]
- >
- > print('Not installed:')
- > for f in sorted(set(tracked) - set(installed)):
- > print(' %s' % pycompat.sysstr(f))
- >
- > print('Not tracked:')
- > for f in sorted(set(installed) - set(tracked)):
- > print(' %s' % pycompat.sysstr(f))
- > EOF
-
- $ ( testrepohgenv; "$PYTHON" wixxml.py help )
- Not installed:
- help/common.txt
- help/hg-ssh.8.txt
- help/hg.1.txt
- help/hgignore.5.txt
- help/hgrc.5.txt
- Not tracked:
-
- $ ( testrepohgenv; "$PYTHON" wixxml.py templates )
- Not installed:
- Not tracked:
-
-#endif
-
#if py3
$ HGALLOWPYTHON3=1
$ export HGALLOWPYTHON3