changeset 44773:16cba0ad8ac2

merge with stable
author Martin von Zweigbergk <martinvonz@google.com>
date Fri, 01 May 2020 08:07:25 -0700
parents 45f3f35cefe7 (current diff) 5e788dc7fb5d (diff)
children cfe86fbc722e
files
diffstat 15 files changed, 848 insertions(+), 296 deletions(-) [+]
line wrap: on
line diff
--- a/contrib/automation/hgautomation/cli.py	Thu Mar 05 17:55:05 2020 +0100
+++ b/contrib/automation/hgautomation/cli.py	Fri May 01 08:07:25 2020 -0700
@@ -63,7 +63,13 @@
 
 
 def build_inno(
-    hga: HGAutomation, aws_region, arch, revision, version, base_image_name
+    hga: HGAutomation,
+    aws_region,
+    python_version,
+    arch,
+    revision,
+    version,
+    base_image_name,
 ):
     c = hga.aws_connection(aws_region)
     image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
@@ -74,14 +80,25 @@
 
         windows.synchronize_hg(SOURCE_ROOT, revision, instance)
 
-        for a in arch:
-            windows.build_inno_installer(
-                instance.winrm_client, a, DIST_PATH, version=version
-            )
+        for py_version in python_version:
+            for a in arch:
+                windows.build_inno_installer(
+                    instance.winrm_client,
+                    py_version,
+                    a,
+                    DIST_PATH,
+                    version=version,
+                )
 
 
 def build_wix(
-    hga: HGAutomation, aws_region, arch, revision, version, base_image_name
+    hga: HGAutomation,
+    aws_region,
+    python_version,
+    arch,
+    revision,
+    version,
+    base_image_name,
 ):
     c = hga.aws_connection(aws_region)
     image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
@@ -92,14 +109,24 @@
 
         windows.synchronize_hg(SOURCE_ROOT, revision, instance)
 
-        for a in arch:
-            windows.build_wix_installer(
-                instance.winrm_client, a, DIST_PATH, version=version
-            )
+        for py_version in python_version:
+            for a in arch:
+                windows.build_wix_installer(
+                    instance.winrm_client,
+                    py_version,
+                    a,
+                    DIST_PATH,
+                    version=version,
+                )
 
 
 def build_windows_wheel(
-    hga: HGAutomation, aws_region, arch, revision, base_image_name
+    hga: HGAutomation,
+    aws_region,
+    python_version,
+    arch,
+    revision,
+    base_image_name,
 ):
     c = hga.aws_connection(aws_region)
     image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
@@ -110,8 +137,11 @@
 
         windows.synchronize_hg(SOURCE_ROOT, revision, instance)
 
-        for a in arch:
-            windows.build_wheel(instance.winrm_client, a, DIST_PATH)
+        for py_version in python_version:
+            for a in arch:
+                windows.build_wheel(
+                    instance.winrm_client, py_version, a, DIST_PATH
+                )
 
 
 def build_all_windows_packages(
@@ -128,17 +158,25 @@
 
         windows.synchronize_hg(SOURCE_ROOT, revision, instance)
 
-        for arch in ('x86', 'x64'):
-            windows.purge_hg(winrm_client)
-            windows.build_wheel(winrm_client, arch, DIST_PATH)
-            windows.purge_hg(winrm_client)
-            windows.build_inno_installer(
-                winrm_client, arch, DIST_PATH, version=version
-            )
-            windows.purge_hg(winrm_client)
-            windows.build_wix_installer(
-                winrm_client, arch, DIST_PATH, version=version
-            )
+        for py_version in ("2.7", "3.7", "3.8"):
+            for arch in ("x86", "x64"):
+                windows.purge_hg(winrm_client)
+                windows.build_wheel(
+                    winrm_client,
+                    python_version=py_version,
+                    arch=arch,
+                    dest_path=DIST_PATH,
+                )
+
+        for py_version in (2, 3):
+            for arch in ('x86', 'x64'):
+                windows.purge_hg(winrm_client)
+                windows.build_inno_installer(
+                    winrm_client, py_version, arch, DIST_PATH, version=version
+                )
+                windows.build_wix_installer(
+                    winrm_client, py_version, arch, DIST_PATH, version=version
+                )
 
 
 def terminate_ec2_instances(hga: HGAutomation, aws_region):
@@ -293,6 +331,14 @@
         'build-inno', help='Build Inno Setup installer(s)',
     )
     sp.add_argument(
+        '--python-version',
+        help='Which version of Python to target',
+        choices={2, 3},
+        type=int,
+        nargs='*',
+        default=[3],
+    )
+    sp.add_argument(
         '--arch',
         help='Architecture to build for',
         choices={'x86', 'x64'},
@@ -316,6 +362,13 @@
         'build-windows-wheel', help='Build Windows wheel(s)',
     )
     sp.add_argument(
+        '--python-version',
+        help='Python version to build for',
+        choices={'2.7', '3.7', '3.8'},
+        nargs='*',
+        default=['3.8'],
+    )
+    sp.add_argument(
         '--arch',
         help='Architecture to build for',
         choices={'x86', 'x64'},
@@ -334,6 +387,14 @@
 
     sp = subparsers.add_parser('build-wix', help='Build WiX installer(s)')
     sp.add_argument(
+        '--python-version',
+        help='Which version of Python to target',
+        choices={2, 3},
+        type=int,
+        nargs='*',
+        default=[3],
+    )
+    sp.add_argument(
         '--arch',
         help='Architecture to build for',
         choices={'x86', 'x64'},
--- a/contrib/automation/hgautomation/linux.py	Thu Mar 05 17:55:05 2020 +0100
+++ b/contrib/automation/hgautomation/linux.py	Fri May 01 08:07:25 2020 -0700
@@ -72,8 +72,10 @@
 
 chmod +x rustup-init
 sudo -H -u hg -g hg ./rustup-init -y
-sudo -H -u hg -g hg /home/hg/.cargo/bin/rustup install 1.31.1 1.34.2
+sudo -H -u hg -g hg /home/hg/.cargo/bin/rustup install 1.31.1 1.42.0
 sudo -H -u hg -g hg /home/hg/.cargo/bin/rustup component add clippy
+
+sudo -H -u hg -g hg /home/hg/.cargo/bin/cargo install --version 0.7.0 pyoxidizer
 '''
 
 
--- a/contrib/automation/hgautomation/windows.py	Thu Mar 05 17:55:05 2020 +0100
+++ b/contrib/automation/hgautomation/windows.py	Fri May 01 08:07:25 2020 -0700
@@ -68,10 +68,20 @@
 Write-Output "updated Mercurial working directory to {revision}"
 '''.lstrip()
 
-BUILD_INNO = r'''
+BUILD_INNO_PYTHON3 = r'''
+$Env:RUSTUP_HOME = "C:\hgdev\rustup"
+$Env:CARGO_HOME = "C:\hgdev\cargo"
+Set-Location C:\hgdev\src
+C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py inno --pyoxidizer-target {pyoxidizer_target} --version {version}
+if ($LASTEXITCODE -ne 0) {{
+    throw "process exited non-0: $LASTEXITCODE"
+}}
+'''
+
+BUILD_INNO_PYTHON2 = r'''
 Set-Location C:\hgdev\src
 $python = "C:\hgdev\python27-{arch}\python.exe"
-C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py inno --python $python
+C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py inno --python $python {extra_args}
 if ($LASTEXITCODE -ne 0) {{
     throw "process exited non-0: $LASTEXITCODE"
 }}
@@ -79,13 +89,23 @@
 
 BUILD_WHEEL = r'''
 Set-Location C:\hgdev\src
-C:\hgdev\python27-{arch}\Scripts\pip.exe wheel --wheel-dir dist .
+C:\hgdev\python{python_version}-{arch}\python.exe -m pip wheel --wheel-dir dist .
 if ($LASTEXITCODE -ne 0) {{
     throw "process exited non-0: $LASTEXITCODE"
 }}
 '''
 
-BUILD_WIX = r'''
+BUILD_WIX_PYTHON3 = r'''
+$Env:RUSTUP_HOME = "C:\hgdev\rustup"
+$Env:CARGO_HOME = "C:\hgdev\cargo"
+Set-Location C:\hgdev\src
+C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py wix --pyoxidizer-target {pyoxidizer_target} --version {version}
+if ($LASTEXITCODE -ne 0) {{
+    throw "process exited non-0: $LASTEXITCODE"
+}}
+'''
+
+BUILD_WIX_PYTHON2 = r'''
 Set-Location C:\hgdev\src
 $python = "C:\hgdev\python27-{arch}\python.exe"
 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py wix --python $python {extra_args}
@@ -101,31 +121,60 @@
 }}
 '''
 
-X86_WHEEL_FILENAME = 'mercurial-{version}-cp27-cp27m-win32.whl'
-X64_WHEEL_FILENAME = 'mercurial-{version}-cp27-cp27m-win_amd64.whl'
-X86_EXE_FILENAME = 'Mercurial-{version}.exe'
-X64_EXE_FILENAME = 'Mercurial-{version}-x64.exe'
-X86_MSI_FILENAME = 'mercurial-{version}-x86.msi'
-X64_MSI_FILENAME = 'mercurial-{version}-x64.msi'
+WHEEL_FILENAME_PYTHON27_X86 = 'mercurial-{version}-cp27-cp27m-win32.whl'
+WHEEL_FILENAME_PYTHON27_X64 = 'mercurial-{version}-cp27-cp27m-win_amd64.whl'
+WHEEL_FILENAME_PYTHON37_X86 = 'mercurial-{version}-cp37-cp37m-win32.whl'
+WHEEL_FILENAME_PYTHON37_X64 = 'mercurial-{version}-cp37-cp37m-win_amd64.whl'
+WHEEL_FILENAME_PYTHON38_X86 = 'mercurial-{version}-cp38-cp38-win32.whl'
+WHEEL_FILENAME_PYTHON38_X64 = 'mercurial-{version}-cp38-cp38-win_amd64.whl'
+
+EXE_FILENAME_PYTHON2_X86 = 'Mercurial-{version}-x86-python2.exe'
+EXE_FILENAME_PYTHON2_X64 = 'Mercurial-{version}-x64-python2.exe'
+EXE_FILENAME_PYTHON3_X86 = 'Mercurial-{version}-x86.exe'
+EXE_FILENAME_PYTHON3_X64 = 'Mercurial-{version}-x64.exe'
+
+MSI_FILENAME_PYTHON2_X86 = 'mercurial-{version}-x86-python2.msi'
+MSI_FILENAME_PYTHON2_X64 = 'mercurial-{version}-x64-python2.msi'
+MSI_FILENAME_PYTHON3_X86 = 'mercurial-{version}-x86.msi'
+MSI_FILENAME_PYTHON3_X64 = 'mercurial-{version}-x64.msi'
 
 MERCURIAL_SCM_BASE_URL = 'https://mercurial-scm.org/release/windows'
 
 X86_USER_AGENT_PATTERN = '.*Windows.*'
 X64_USER_AGENT_PATTERN = '.*Windows.*(WOW|x)64.*'
 
-X86_EXE_DESCRIPTION = (
-    'Mercurial {version} Inno Setup installer - x86 Windows '
+EXE_PYTHON2_X86_DESCRIPTION = (
+    'Mercurial {version} Inno Setup installer - x86 Windows (Python 2) '
+    '- does not require admin rights'
+)
+EXE_PYTHON2_X64_DESCRIPTION = (
+    'Mercurial {version} Inno Setup installer - x64 Windows (Python 2) '
+    '- does not require admin rights'
+)
+# TODO remove Python version once Python 2 is dropped.
+EXE_PYTHON3_X86_DESCRIPTION = (
+    'Mercurial {version} Inno Setup installer - x86 Windows (Python 3) '
     '- does not require admin rights'
 )
-X64_EXE_DESCRIPTION = (
-    'Mercurial {version} Inno Setup installer - x64 Windows '
+EXE_PYTHON3_X64_DESCRIPTION = (
+    'Mercurial {version} Inno Setup installer - x64 Windows (Python 3) '
     '- does not require admin rights'
 )
-X86_MSI_DESCRIPTION = (
-    'Mercurial {version} MSI installer - x86 Windows ' '- requires admin rights'
+MSI_PYTHON2_X86_DESCRIPTION = (
+    'Mercurial {version} MSI installer - x86 Windows (Python 2) '
+    '- requires admin rights'
+)
+MSI_PYTHON2_X64_DESCRIPTION = (
+    'Mercurial {version} MSI installer - x64 Windows (Python 2) '
+    '- requires admin rights'
 )
-X64_MSI_DESCRIPTION = (
-    'Mercurial {version} MSI installer - x64 Windows ' '- requires admin rights'
+MSI_PYTHON3_X86_DESCRIPTION = (
+    'Mercurial {version} MSI installer - x86 Windows (Python 3) '
+    '- requires admin rights'
+)
+MSI_PYTHON3_X64_DESCRIPTION = (
+    'Mercurial {version} MSI installer - x64 Windows (Python 3) '
+    '- requires admin rights'
 )
 
 
@@ -280,53 +329,113 @@
 
 
 def build_inno_installer(
-    winrm_client, arch: str, dest_path: pathlib.Path, version=None
+    winrm_client,
+    python_version: int,
+    arch: str,
+    dest_path: pathlib.Path,
+    version=None,
 ):
     """Build the Inno Setup installer on a remote machine.
 
     Using a WinRM client, remote commands are executed to build
     a Mercurial Inno Setup installer.
     """
-    print('building Inno Setup installer for %s' % arch)
+    print(
+        'building Inno Setup installer for Python %d %s'
+        % (python_version, arch)
+    )
+
+    if python_version == 3:
+        # TODO fix this limitation in packaging code
+        if not version:
+            raise Exception(
+                "version string is required when building for Python 3"
+            )
 
-    extra_args = []
-    if version:
-        extra_args.extend(['--version', version])
+        if arch == "x86":
+            target_triple = "i686-pc-windows-msvc"
+        elif arch == "x64":
+            target_triple = "x86_64-pc-windows-msvc"
+        else:
+            raise Exception("unhandled arch: %s" % arch)
 
-    ps = get_vc_prefix(arch) + BUILD_INNO.format(
-        arch=arch, extra_args=' '.join(extra_args)
-    )
+        ps = BUILD_INNO_PYTHON3.format(
+            pyoxidizer_target=target_triple, version=version,
+        )
+    else:
+        extra_args = []
+        if version:
+            extra_args.extend(['--version', version])
+
+        ps = get_vc_prefix(arch) + BUILD_INNO_PYTHON2.format(
+            arch=arch, extra_args=' '.join(extra_args)
+        )
+
     run_powershell(winrm_client, ps)
     copy_latest_dist(winrm_client, '*.exe', dest_path)
 
 
-def build_wheel(winrm_client, arch: str, dest_path: pathlib.Path):
+def build_wheel(
+    winrm_client, python_version: str, arch: str, dest_path: pathlib.Path
+):
     """Build Python wheels on a remote machine.
 
     Using a WinRM client, remote commands are executed to build a Python wheel
     for Mercurial.
     """
-    print('Building Windows wheel for %s' % arch)
-    ps = get_vc_prefix(arch) + BUILD_WHEEL.format(arch=arch)
+    print('Building Windows wheel for Python %s %s' % (python_version, arch))
+
+    ps = BUILD_WHEEL.format(
+        python_version=python_version.replace(".", ""), arch=arch
+    )
+
+    # Python 2.7 requires an activated environment.
+    if python_version == "2.7":
+        ps = get_vc_prefix(arch) + ps
+
     run_powershell(winrm_client, ps)
     copy_latest_dist(winrm_client, '*.whl', dest_path)
 
 
 def build_wix_installer(
-    winrm_client, arch: str, dest_path: pathlib.Path, version=None
+    winrm_client,
+    python_version: int,
+    arch: str,
+    dest_path: pathlib.Path,
+    version=None,
 ):
     """Build the WiX installer on a remote machine.
 
     Using a WinRM client, remote commands are executed to build a WiX installer.
     """
-    print('Building WiX installer for %s' % arch)
-    extra_args = []
-    if version:
-        extra_args.extend(['--version', version])
+    print('Building WiX installer for Python %d %s' % (python_version, arch))
+
+    if python_version == 3:
+        # TODO fix this limitation in packaging code
+        if not version:
+            raise Exception(
+                "version string is required when building for Python 3"
+            )
 
-    ps = get_vc_prefix(arch) + BUILD_WIX.format(
-        arch=arch, extra_args=' '.join(extra_args)
-    )
+        if arch == "x86":
+            target_triple = "i686-pc-windows-msvc"
+        elif arch == "x64":
+            target_triple = "x86_64-pc-windows-msvc"
+        else:
+            raise Exception("unhandled arch: %s" % arch)
+
+        ps = BUILD_WIX_PYTHON3.format(
+            pyoxidizer_target=target_triple, version=version,
+        )
+    else:
+        extra_args = []
+        if version:
+            extra_args.extend(['--version', version])
+
+        ps = get_vc_prefix(arch) + BUILD_WIX_PYTHON2.format(
+            arch=arch, extra_args=' '.join(extra_args)
+        )
+
     run_powershell(winrm_client, ps)
     copy_latest_dist(winrm_client, '*.msi', dest_path)
 
@@ -356,56 +465,100 @@
 
 def resolve_wheel_artifacts(dist_path: pathlib.Path, version: str):
     return (
-        dist_path / X86_WHEEL_FILENAME.format(version=version),
-        dist_path / X64_WHEEL_FILENAME.format(version=version),
+        dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
+        dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
+        dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
+        dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
+        dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
+        dist_path / WHEEL_FILENAME_PYTHON38_X64.format(version=version),
     )
 
 
 def resolve_all_artifacts(dist_path: pathlib.Path, version: str):
     return (
-        dist_path / X86_WHEEL_FILENAME.format(version=version),
-        dist_path / X64_WHEEL_FILENAME.format(version=version),
-        dist_path / X86_EXE_FILENAME.format(version=version),
-        dist_path / X64_EXE_FILENAME.format(version=version),
-        dist_path / X86_MSI_FILENAME.format(version=version),
-        dist_path / X64_MSI_FILENAME.format(version=version),
+        dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
+        dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
+        dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
+        dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
+        dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
+        dist_path / WHEEL_FILENAME_PYTHON38_X64.format(version=version),
+        dist_path / EXE_FILENAME_PYTHON2_X86.format(version=version),
+        dist_path / EXE_FILENAME_PYTHON2_X64.format(version=version),
+        dist_path / EXE_FILENAME_PYTHON3_X86.format(version=version),
+        dist_path / EXE_FILENAME_PYTHON3_X64.format(version=version),
+        dist_path / MSI_FILENAME_PYTHON2_X86.format(version=version),
+        dist_path / MSI_FILENAME_PYTHON2_X64.format(version=version),
+        dist_path / MSI_FILENAME_PYTHON3_X86.format(version=version),
+        dist_path / MSI_FILENAME_PYTHON3_X64.format(version=version),
     )
 
 
 def generate_latest_dat(version: str):
-    x86_exe_filename = X86_EXE_FILENAME.format(version=version)
-    x64_exe_filename = X64_EXE_FILENAME.format(version=version)
-    x86_msi_filename = X86_MSI_FILENAME.format(version=version)
-    x64_msi_filename = X64_MSI_FILENAME.format(version=version)
+    python2_x86_exe_filename = EXE_FILENAME_PYTHON2_X86.format(version=version)
+    python2_x64_exe_filename = EXE_FILENAME_PYTHON2_X64.format(version=version)
+    python3_x86_exe_filename = EXE_FILENAME_PYTHON3_X86.format(version=version)
+    python3_x64_exe_filename = EXE_FILENAME_PYTHON3_X64.format(version=version)
+    python2_x86_msi_filename = MSI_FILENAME_PYTHON2_X86.format(version=version)
+    python2_x64_msi_filename = MSI_FILENAME_PYTHON2_X64.format(version=version)
+    python3_x86_msi_filename = MSI_FILENAME_PYTHON3_X86.format(version=version)
+    python3_x64_msi_filename = MSI_FILENAME_PYTHON3_X64.format(version=version)
 
     entries = (
         (
             '10',
             version,
             X86_USER_AGENT_PATTERN,
-            '%s/%s' % (MERCURIAL_SCM_BASE_URL, x86_exe_filename),
-            X86_EXE_DESCRIPTION.format(version=version),
+            '%s/%s' % (MERCURIAL_SCM_BASE_URL, python3_x86_exe_filename),
+            EXE_PYTHON3_X86_DESCRIPTION.format(version=version),
         ),
         (
             '10',
             version,
             X64_USER_AGENT_PATTERN,
-            '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_exe_filename),
-            X64_EXE_DESCRIPTION.format(version=version),
+            '%s/%s' % (MERCURIAL_SCM_BASE_URL, python3_x64_exe_filename),
+            EXE_PYTHON3_X64_DESCRIPTION.format(version=version),
+        ),
+        (
+            '9',
+            version,
+            X86_USER_AGENT_PATTERN,
+            '%s/%s' % (MERCURIAL_SCM_BASE_URL, python2_x86_exe_filename),
+            EXE_PYTHON2_X86_DESCRIPTION.format(version=version),
+        ),
+        (
+            '9',
+            version,
+            X64_USER_AGENT_PATTERN,
+            '%s/%s' % (MERCURIAL_SCM_BASE_URL, python2_x64_exe_filename),
+            EXE_PYTHON2_X64_DESCRIPTION.format(version=version),
         ),
         (
             '10',
             version,
             X86_USER_AGENT_PATTERN,
-            '%s/%s' % (MERCURIAL_SCM_BASE_URL, x86_msi_filename),
-            X86_MSI_DESCRIPTION.format(version=version),
+            '%s/%s' % (MERCURIAL_SCM_BASE_URL, python3_x86_msi_filename),
+            MSI_PYTHON3_X86_DESCRIPTION.format(version=version),
         ),
         (
             '10',
             version,
             X64_USER_AGENT_PATTERN,
-            '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_msi_filename),
-            X64_MSI_DESCRIPTION.format(version=version),
+            '%s/%s' % (MERCURIAL_SCM_BASE_URL, python3_x64_msi_filename),
+            MSI_PYTHON3_X64_DESCRIPTION.format(version=version),
+        ),
+        (
+            '9',
+            version,
+            X86_USER_AGENT_PATTERN,
+            '%s/%s' % (MERCURIAL_SCM_BASE_URL, python2_x86_msi_filename),
+            MSI_PYTHON2_X86_DESCRIPTION.format(version=version),
+        ),
+        (
+            '9',
+            version,
+            X64_USER_AGENT_PATTERN,
+            '%s/%s' % (MERCURIAL_SCM_BASE_URL, python2_x64_msi_filename),
+            MSI_PYTHON2_X64_DESCRIPTION.format(version=version),
         ),
     )
 
--- a/contrib/install-windows-dependencies.ps1	Thu Mar 05 17:55:05 2020 +0100
+++ b/contrib/install-windows-dependencies.ps1	Fri May 01 08:07:25 2020 -0700
@@ -64,6 +64,9 @@
 $MERCURIAL_WHEEL_URL = "https://files.pythonhosted.org/packages/6d/47/e031e47f7fe9b16e4e3383da47e2b0a7eae6e603996bc67a03ec4fa1b3f4/$MERCURIAL_WHEEL_FILENAME"
 $MERCURIAL_WHEEL_SHA256 = "1d18c7f6ca1456f0f62ee65c9a50c14cbba48ce6e924930cdb10537f5c9eaf5f"
 
+$RUSTUP_INIT_URL = "https://static.rust-lang.org/rustup/archive/1.21.1/x86_64-pc-windows-gnu/rustup-init.exe"
+$RUSTUP_INIT_SHA256 = "d17df34ba974b9b19cf5c75883a95475aa22ddc364591d75d174090d55711c72"
+
 # Writing progress slows down downloads substantially. So disable it.
 $progressPreference = 'silentlyContinue'
 
@@ -116,6 +119,20 @@
     Invoke-Process ${dest}\python.exe $pip
 }
 
+function Install-Rust($prefix) {
+    Write-Output "installing Rust"
+    $Env:RUSTUP_HOME = "${prefix}\rustup"
+    $Env:CARGO_HOME = "${prefix}\cargo"
+
+    Invoke-Process "${prefix}\assets\rustup-init.exe" "-y --default-host x86_64-pc-windows-msvc"
+    Invoke-Process "${prefix}\cargo\bin\rustup.exe" "target add i686-pc-windows-msvc"
+    Invoke-Process "${prefix}\cargo\bin\rustup.exe" "install 1.42.0"
+    Invoke-Process "${prefix}\cargo\bin\rustup.exe" "component add clippy"
+
+    # Install PyOxidizer for packaging.
+    Invoke-Process "${prefix}\cargo\bin\cargo.exe" "install --version 0.7.0 pyoxidizer"
+}
+
 function Install-Dependencies($prefix) {
     if (!(Test-Path -Path $prefix\assets)) {
         New-Item -Path $prefix\assets -ItemType Directory
@@ -140,6 +157,7 @@
     Secure-Download $INNO_SETUP_URL ${prefix}\assets\InnoSetup.exe $INNO_SETUP_SHA256
     Secure-Download $MINGW_BIN_URL ${prefix}\assets\mingw-get-bin.zip $MINGW_BIN_SHA256
     Secure-Download $MERCURIAL_WHEEL_URL ${prefix}\assets\${MERCURIAL_WHEEL_FILENAME} $MERCURIAL_WHEEL_SHA256
+    Secure-Download $RUSTUP_INIT_URL ${prefix}\assets\rustup-init.exe $RUSTUP_INIT_SHA256
 
     Write-Output "installing Python 2.7 32-bit"
     Invoke-Process msiexec.exe "/i ${prefix}\assets\python27-x86.msi /l* ${prefix}\assets\python27-x86.log /q TARGETDIR=${prefix}\python27-x86 ALLUSERS="
@@ -163,6 +181,8 @@
     Write-Output "installing Visual Studio 2017 Build Tools and SDKs"
     Invoke-Process ${prefix}\assets\vs_buildtools.exe "--quiet --wait --norestart --nocache --channelUri https://aka.ms/vs/15/release/channel --add Microsoft.VisualStudio.Workload.MSBuildTools --add Microsoft.VisualStudio.Component.Windows10SDK.17763 --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Component.Windows10SDK --add Microsoft.VisualStudio.Component.VC.140"
 
+    Install-Rust ${prefix}
+
     Write-Output "installing Visual C++ 9.0 for Python 2.7"
     Invoke-Process msiexec.exe "/i ${prefix}\assets\VCForPython27.msi /l* ${prefix}\assets\VCForPython27.log /q"
 
--- a/contrib/packaging/hgpackaging/cli.py	Thu Mar 05 17:55:05 2020 +0100
+++ b/contrib/packaging/hgpackaging/cli.py	Fri May 01 08:07:25 2020 -0700
@@ -20,8 +20,11 @@
 SOURCE_DIR = HERE.parent.parent.parent
 
 
-def build_inno(python=None, iscc=None, version=None):
-    if not os.path.isabs(python):
+def build_inno(pyoxidizer_target=None, python=None, iscc=None, version=None):
+    if not pyoxidizer_target and not python:
+        raise Exception("--python required unless building with PyOxidizer")
+
+    if python and not os.path.isabs(python):
         raise Exception("--python arg must be an absolute path")
 
     if iscc:
@@ -35,13 +38,19 @@
 
     build_dir = SOURCE_DIR / "build"
 
-    inno.build(
-        SOURCE_DIR, build_dir, pathlib.Path(python), iscc, version=version,
-    )
+    if pyoxidizer_target:
+        inno.build_with_pyoxidizer(
+            SOURCE_DIR, build_dir, pyoxidizer_target, iscc, version=version
+        )
+    else:
+        inno.build_with_py2exe(
+            SOURCE_DIR, build_dir, pathlib.Path(python), iscc, version=version,
+        )
 
 
 def build_wix(
     name=None,
+    pyoxidizer_target=None,
     python=None,
     version=None,
     sign_sn=None,
@@ -52,17 +61,29 @@
     extra_wxs=None,
     extra_features=None,
 ):
-    fn = wix.build_installer
+    if not pyoxidizer_target and not python:
+        raise Exception("--python required unless building with PyOxidizer")
+
+    if python and not os.path.isabs(python):
+        raise Exception("--python arg must be an absolute path")
+
     kwargs = {
         "source_dir": SOURCE_DIR,
-        "python_exe": pathlib.Path(python),
         "version": version,
     }
 
-    if not os.path.isabs(python):
-        raise Exception("--python arg must be an absolute path")
+    if pyoxidizer_target:
+        fn = wix.build_installer_pyoxidizer
+        kwargs["target_triple"] = pyoxidizer_target
+    else:
+        fn = wix.build_installer_py2exe
+        kwargs["python_exe"] = pathlib.Path(python)
 
     if extra_packages_script:
+        if pyoxidizer_target:
+            raise Exception(
+                "pyoxidizer does not support --extra-packages-script"
+            )
         kwargs["extra_packages_script"] = extra_packages_script
     if extra_wxs:
         kwargs["extra_wxs"] = dict(
@@ -72,12 +93,13 @@
         kwargs["extra_features"] = extra_features.split(",")
 
     if sign_sn or sign_cert:
-        fn = wix.build_signed_installer
-        kwargs["name"] = name
-        kwargs["subject_name"] = sign_sn
-        kwargs["cert_path"] = sign_cert
-        kwargs["cert_password"] = sign_password
-        kwargs["timestamp_url"] = sign_timestamp_url
+        kwargs["signing_info"] = {
+            "name": name,
+            "subject_name": sign_sn,
+            "cert_path": sign_cert,
+            "cert_password": sign_password,
+            "timestamp_url": sign_timestamp_url,
+        }
 
     fn(**kwargs)
 
@@ -88,7 +110,12 @@
     subparsers = parser.add_subparsers()
 
     sp = subparsers.add_parser("inno", help="Build Inno Setup installer")
-    sp.add_argument("--python", required=True, help="path to python.exe to use")
+    sp.add_argument(
+        "--pyoxidizer-target",
+        choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
+        help="Build with PyOxidizer targeting this host triple",
+    )
+    sp.add_argument("--python", help="path to python.exe to use")
     sp.add_argument("--iscc", help="path to iscc.exe to use")
     sp.add_argument(
         "--version",
@@ -102,8 +129,11 @@
     )
     sp.add_argument("--name", help="Application name", default="Mercurial")
     sp.add_argument(
-        "--python", help="Path to Python executable to use", required=True
+        "--pyoxidizer-target",
+        choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
+        help="Build with PyOxidizer targeting this host triple",
     )
+    sp.add_argument("--python", help="Path to Python executable to use")
     sp.add_argument(
         "--sign-sn",
         help="Subject name (or fragment thereof) of certificate "
--- a/contrib/packaging/hgpackaging/inno.py	Thu Mar 05 17:55:05 2020 +0100
+++ b/contrib/packaging/hgpackaging/inno.py	Fri May 01 08:07:25 2020 -0700
@@ -18,8 +18,9 @@
     build_py2exe,
     stage_install,
 )
+from .pyoxidizer import run_pyoxidizer
 from .util import (
-    find_vc_runtime_files,
+    find_legacy_vc_runtime_files,
     normalize_windows_version,
     process_install_rules,
     read_version_py,
@@ -41,14 +42,14 @@
 }
 
 
-def build(
+def build_with_py2exe(
     source_dir: pathlib.Path,
     build_dir: pathlib.Path,
     python_exe: pathlib.Path,
     iscc_exe: pathlib.Path,
     version=None,
 ):
-    """Build the Inno installer.
+    """Build the Inno installer using py2exe.
 
     Build files will be placed in ``build_dir``.
 
@@ -61,8 +62,7 @@
 
     vc_x64 = r'\x64' in os.environ.get('LIB', '')
     arch = 'x64' if vc_x64 else 'x86'
-    inno_source_dir = source_dir / 'contrib' / 'packaging' / 'inno'
-    inno_build_dir = build_dir / ('inno-%s' % arch)
+    inno_build_dir = build_dir / ('inno-py2exe-%s' % arch)
     staging_dir = inno_build_dir / 'stage'
 
     requirements_txt = (
@@ -93,7 +93,7 @@
     process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
 
     # hg.exe depends on VC9 runtime DLLs. Copy those into place.
-    for f in find_vc_runtime_files(vc_x64):
+    for f in find_legacy_vc_runtime_files(vc_x64):
         if f.name.endswith('.manifest'):
             basename = 'Microsoft.VC90.CRT.manifest'
         else:
@@ -104,6 +104,62 @@
         print('copying %s to %s' % (f, dest_path))
         shutil.copyfile(f, dest_path)
 
+    build_installer(
+        source_dir,
+        inno_build_dir,
+        staging_dir,
+        iscc_exe,
+        version,
+        arch="x64" if vc_x64 else None,
+        suffix="-python2",
+    )
+
+
+def build_with_pyoxidizer(
+    source_dir: pathlib.Path,
+    build_dir: pathlib.Path,
+    target_triple: str,
+    iscc_exe: pathlib.Path,
+    version=None,
+):
+    """Build the Inno installer using PyOxidizer."""
+    if not iscc_exe.exists():
+        raise Exception("%s does not exist" % iscc_exe)
+
+    inno_build_dir = build_dir / ("inno-pyoxidizer-%s" % target_triple)
+    staging_dir = inno_build_dir / "stage"
+
+    inno_build_dir.mkdir(parents=True, exist_ok=True)
+    run_pyoxidizer(source_dir, inno_build_dir, staging_dir, target_triple)
+
+    process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
+
+    build_installer(
+        source_dir,
+        inno_build_dir,
+        staging_dir,
+        iscc_exe,
+        version,
+        arch="x64" if "x86_64" in target_triple else None,
+    )
+
+
+def build_installer(
+    source_dir: pathlib.Path,
+    inno_build_dir: pathlib.Path,
+    staging_dir: pathlib.Path,
+    iscc_exe: pathlib.Path,
+    version,
+    arch=None,
+    suffix="",
+):
+    """Build an Inno installer from staged Mercurial files.
+
+    This function is agnostic about how to build Mercurial. It just
+    cares that Mercurial files are in ``staging_dir``.
+    """
+    inno_source_dir = source_dir / "contrib" / "packaging" / "inno"
+
     # The final package layout is simply a mirror of the staging directory.
     package_files = []
     for root, dirs, files in os.walk(staging_dir):
@@ -158,8 +214,11 @@
 
     args = [str(iscc_exe)]
 
-    if vc_x64:
-        args.append('/dARCH=x64')
+    if arch:
+        args.append('/dARCH=%s' % arch)
+        args.append('/dSUFFIX=-%s%s' % (arch, suffix))
+    else:
+        args.append('/dSUFFIX=-x86%s' % suffix)
 
     if not version:
         version = read_version_py(source_dir)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/packaging/hgpackaging/pyoxidizer.py	Fri May 01 08:07:25 2020 -0700
@@ -0,0 +1,145 @@
+# pyoxidizer.py - Packaging support for PyOxidizer
+#
+# Copyright 2020 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 shutil
+import subprocess
+import sys
+
+from .downloads import download_entry
+from .util import (
+    extract_zip_to_directory,
+    process_install_rules,
+    find_vc_runtime_dll,
+)
+
+
+STAGING_RULES_WINDOWS = [
+    ('contrib/bash_completion', 'contrib/'),
+    ('contrib/hgk', 'contrib/hgk.tcl'),
+    ('contrib/hgweb.fcgi', 'contrib/'),
+    ('contrib/hgweb.wsgi', 'contrib/'),
+    ('contrib/logo-droplets.svg', 'contrib/'),
+    ('contrib/mercurial.el', 'contrib/'),
+    ('contrib/mq.el', 'contrib/'),
+    ('contrib/tcsh_completion', 'contrib/'),
+    ('contrib/tcsh_completion_build.sh', 'contrib/'),
+    ('contrib/vim/*', 'contrib/vim/'),
+    ('contrib/win32/postinstall.txt', 'ReleaseNotes.txt'),
+    ('contrib/win32/ReadMe.html', 'ReadMe.html'),
+    ('contrib/xml.rnc', 'contrib/'),
+    ('contrib/zsh_completion', 'contrib/'),
+    ('doc/*.html', 'doc/'),
+    ('doc/style.css', 'doc/'),
+    ('COPYING', 'Copying.txt'),
+]
+
+STAGING_RULES_APP = [
+    ('mercurial/helptext/**/*.txt', 'helptext/'),
+    ('mercurial/defaultrc/*.rc', 'defaultrc/'),
+    ('mercurial/locale/**/*', 'locale/'),
+    ('mercurial/templates/**/*', 'templates/'),
+]
+
+STAGING_EXCLUDES_WINDOWS = [
+    "doc/hg-ssh.8.html",
+]
+
+
+def run_pyoxidizer(
+    source_dir: pathlib.Path,
+    build_dir: pathlib.Path,
+    out_dir: pathlib.Path,
+    target_triple: str,
+):
+    """Build Mercurial with PyOxidizer and copy additional files into place.
+
+    After successful completion, ``out_dir`` contains files constituting a
+    Mercurial install.
+    """
+    # We need to make gettext binaries available for compiling i18n files.
+    gettext_pkg, gettext_entry = download_entry('gettext', build_dir)
+    gettext_dep_pkg = download_entry('gettext-dep', build_dir)[0]
+
+    gettext_root = build_dir / ('gettext-win-%s' % gettext_entry['version'])
+
+    if not gettext_root.exists():
+        extract_zip_to_directory(gettext_pkg, gettext_root)
+        extract_zip_to_directory(gettext_dep_pkg, gettext_root)
+
+    env = dict(os.environ)
+    env["PATH"] = "%s%s%s" % (
+        env["PATH"],
+        os.pathsep,
+        str(gettext_root / "bin"),
+    )
+
+    args = [
+        "pyoxidizer",
+        "build",
+        "--path",
+        str(source_dir / "rust" / "hgcli"),
+        "--release",
+        "--target-triple",
+        target_triple,
+    ]
+
+    subprocess.run(args, env=env, check=True)
+
+    if "windows" in target_triple:
+        target = "app_windows"
+    else:
+        target = "app_posix"
+
+    build_dir = (
+        source_dir / "build" / "pyoxidizer" / target_triple / "release" / target
+    )
+
+    if out_dir.exists():
+        print("purging %s" % out_dir)
+        shutil.rmtree(out_dir)
+
+    # Now assemble all the files from PyOxidizer into the staging directory.
+    shutil.copytree(build_dir, out_dir)
+
+    # Move some of those files around.
+    process_install_rules(STAGING_RULES_APP, build_dir, out_dir)
+    # Nuke the mercurial/* directory, as we copied resources
+    # to an appropriate location just above.
+    shutil.rmtree(out_dir / "mercurial")
+
+    # We also need to run setup.py build_doc to produce html files,
+    # as they aren't built as part of ``pip install``.
+    # This will fail if docutils isn't installed.
+    subprocess.run(
+        [sys.executable, str(source_dir / "setup.py"), "build_doc", "--html"],
+        cwd=str(source_dir),
+        check=True,
+    )
+
+    if "windows" in target_triple:
+        process_install_rules(STAGING_RULES_WINDOWS, source_dir, out_dir)
+
+        # Write out a default editor.rc file to configure notepad as the
+        # default editor.
+        with (out_dir / "defaultrc" / "editor.rc").open(
+            "w", encoding="utf-8"
+        ) as fh:
+            fh.write("[ui]\neditor = notepad\n")
+
+        for f in STAGING_EXCLUDES_WINDOWS:
+            p = out_dir / f
+            if p.exists():
+                print("removing %s" % p)
+                p.unlink()
+
+        # Add vcruntimeXXX.dll next to executable.
+        vc_runtime_dll = find_vc_runtime_dll(x64="x86_64" in target_triple)
+        shutil.copy(vc_runtime_dll, out_dir / vc_runtime_dll.name)
--- a/contrib/packaging/hgpackaging/util.py	Thu Mar 05 17:55:05 2020 +0100
+++ b/contrib/packaging/hgpackaging/util.py	Fri May 01 08:07:25 2020 -0700
@@ -29,7 +29,59 @@
         zf.extractall(dest)
 
 
-def find_vc_runtime_files(x64=False):
+def find_vc_runtime_dll(x64=False):
+    """Finds Visual C++ Runtime DLL to include in distribution."""
+    # We invoke vswhere to find the latest Visual Studio install.
+    vswhere = (
+        pathlib.Path(os.environ["ProgramFiles(x86)"])
+        / "Microsoft Visual Studio"
+        / "Installer"
+        / "vswhere.exe"
+    )
+
+    if not vswhere.exists():
+        raise Exception(
+            "could not find vswhere.exe: %s does not exist" % vswhere
+        )
+
+    args = [
+        str(vswhere),
+        # -products * is necessary to return results from Build Tools
+        # (as opposed to full IDE installs).
+        "-products",
+        "*",
+        "-requires",
+        "Microsoft.VisualCpp.Redist.14.Latest",
+        "-latest",
+        "-property",
+        "installationPath",
+    ]
+
+    vs_install_path = pathlib.Path(
+        os.fsdecode(subprocess.check_output(args).strip())
+    )
+
+    # This just gets us a path like
+    # C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
+    # Actually vcruntime140.dll is under a path like:
+    # VC\Redist\MSVC\<version>\<arch>\Microsoft.VC14<X>.CRT\vcruntime140.dll.
+
+    arch = "x64" if x64 else "x86"
+
+    search_glob = (
+        r"%s\VC\Redist\MSVC\*\%s\Microsoft.VC14*.CRT\vcruntime140.dll"
+        % (vs_install_path, arch)
+    )
+
+    candidates = glob.glob(search_glob, recursive=True)
+
+    for candidate in reversed(candidates):
+        return pathlib.Path(candidate)
+
+    raise Exception("could not find vcruntime140.dll")
+
+
+def find_legacy_vc_runtime_files(x64=False):
     """Finds Visual C++ Runtime DLLs to include in distribution."""
     winsxs = pathlib.Path(os.environ['SYSTEMROOT']) / 'WinSxS'
 
--- a/contrib/packaging/hgpackaging/wix.py	Thu Mar 05 17:55:05 2020 +0100
+++ b/contrib/packaging/hgpackaging/wix.py	Fri May 01 08:07:25 2020 -0700
@@ -22,6 +22,7 @@
     build_py2exe,
     stage_install,
 )
+from .pyoxidizer import run_pyoxidizer
 from .util import (
     extract_zip_to_directory,
     normalize_windows_version,
@@ -121,30 +122,6 @@
     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
-
-
 def make_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
     """Create XML string listing every file to be installed."""
 
@@ -308,27 +285,23 @@
     return doc.toprettyxml()
 
 
-def build_installer(
+def build_installer_py2exe(
     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,
+    signing_info: typing.Optional[typing.Dict[str, str]] = None,
 ):
-    """Build a WiX MSI installer.
+    """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.
-    ``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
@@ -340,8 +313,6 @@
     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 = (
         source_dir / 'contrib' / 'packaging' / 'requirements_win32.txt'
@@ -357,15 +328,6 @@
         extra_packages_script=extra_packages_script,
     )
 
-    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 post_build_fn:
-        post_build_fn(source_dir, hg_build_dir, dist_dir, version)
-
     build_dir = hg_build_dir / ('wix-%s' % arch)
     staging_dir = build_dir / 'stage'
 
@@ -388,13 +350,112 @@
             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'])
+    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(
+    source_dir: pathlib.Path,
+    target_triple: str,
+    msi_name='mercurial',
+    version=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 PyOxidizer."""
+    hg_build_dir = source_dir / "build"
+    build_dir = hg_build_dir / ("wix-%s" % target_triple)
+    staging_dir = build_dir / "stage"
+
+    arch = "x64" if "x86_64" in target_triple else "x86"
+
+    build_dir.mkdir(parents=True, exist_ok=True)
+    run_pyoxidizer(source_dir, build_dir, staging_dir, target_triple)
+
+    # 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,
+        python2=False,
+        msi_name=msi_name,
+        extra_wxs=extra_wxs,
+        extra_features=extra_features,
+        signing_info=signing_info,
+    )
+
+
+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)
 
-    ensure_vc90_merge_modules(hg_build_dir)
+    if python2:
+        ensure_vc90_merge_modules(build_dir)
 
     source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
 
@@ -413,7 +474,16 @@
     source = wix_dir / 'mercurial.wxs'
     defines['Version'] = version
     defines['Comments'] = 'Installs Mercurial version %s' % version
-    defines['VCRedistSrcDir'] = str(hg_build_dir)
+
+    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)
@@ -421,7 +491,9 @@
     run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
 
     msi_path = (
-        source_dir / 'dist' / ('%s-%s-%s.msi' % (msi_name, orig_version, arch))
+        source_dir
+        / 'dist'
+        / ('%s-%s-%s%s.msi' % (msi_name, orig_version, arch, suffix))
     )
 
     args = [
@@ -448,52 +520,16 @@
 
     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,
     }
-
-
-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,
-    )
--- a/contrib/packaging/inno/mercurial.iss	Thu Mar 05 17:55:05 2020 +0100
+++ b/contrib/packaging/inno/mercurial.iss	Fri May 01 08:07:25 2020 -0700
@@ -9,14 +9,13 @@
 AppCopyright=Copyright 2005-2020 Matt Mackall and others
 AppName=Mercurial
 AppVersion={#VERSION}
+OutputBaseFilename=Mercurial-{#VERSION}{#SUFFIX}
 #if ARCH == "x64"
 AppVerName=Mercurial {#VERSION} (64-bit)
-OutputBaseFilename=Mercurial-{#VERSION}-x64
 ArchitecturesAllowed=x64
 ArchitecturesInstallIn64BitMode=x64
 #else
 AppVerName=Mercurial {#VERSION}
-OutputBaseFilename=Mercurial-{#VERSION}
 #endif
 InfoAfterFile=../postinstall.txt
 LicenseFile=Copying.txt
--- a/contrib/packaging/pyoxidizer.bzl	Thu Mar 05 17:55:05 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-# Instructions:
-#
-# 1. cargo install --version 0.5.0 pyoxidizer
-# 2. cd /path/to/hg
-# 3. pyoxidizer build --path contrib/packaging [--release]
-# 4. Run build/pyoxidizer/<arch>/<debug|release>/app/hg
-#
-# If you need to build again, you need to remove the build/lib.* and
-# build/temp.* directories, otherwise PyOxidizer fails to pick up C
-# extensions. This is a bug in PyOxidizer.
-
-ROOT = CWD + "/../.."
-
-set_build_path(ROOT + "/build/pyoxidizer")
-
-def make_exe():
-    dist = default_python_distribution()
-
-    code = "import hgdemandimport; hgdemandimport.enable(); from mercurial import dispatch; dispatch.run()"
-
-    config = PythonInterpreterConfig(
-        raw_allocator = "system",
-        run_eval = code,
-        # We want to let the user load extensions from the file system
-        filesystem_importer = True,
-        # We need this to make resourceutil happy, since it looks for sys.frozen.
-        sys_frozen = True,
-        legacy_windows_stdio = True,
-    )
-
-    exe = dist.to_python_executable(
-        name = "hg",
-        config = config,
-    )
-
-    # Use setup.py install to build Mercurial and collect Python resources to
-    # embed in the executable.
-    resources = dist.setup_py_install(ROOT)
-    exe.add_python_resources(resources)
-
-    return exe
-
-def make_install(exe):
-    m = FileManifest()
-
-    # `hg` goes in root directory.
-    m.add_python_resource(".", exe)
-
-    templates = glob(
-        include=[ROOT + "/mercurial/templates/**/*"],
-        strip_prefix = ROOT + "/mercurial/",
-    )
-    m.add_manifest(templates)
-
-    return m
-
-register_target("exe", make_exe)
-register_target("app", make_install, depends = ["exe"], default = True)
-
-resolve_targets()
--- a/contrib/packaging/wix/mercurial.wxs	Thu Mar 05 17:55:05 2020 +0100
+++ b/contrib/packaging/wix/mercurial.wxs	Fri May 01 08:07:25 2020 -0700
@@ -79,16 +79,21 @@
         </Directory>
       </Directory>
 
-      <?if $(var.Platform) = "x86" ?>
-        <Merge Id='VCRuntime' DiskId='1' Language='1033'
-              SourceFile='$(var.VCRedistSrcDir)\microsoft.vcxx.crt.x86_msm.msm' />
-        <Merge Id='VCRuntimePolicy' DiskId='1' Language='1033'
-              SourceFile='$(var.VCRedistSrcDir)\policy.x.xx.microsoft.vcxx.crt.x86_msm.msm' />
-      <?else?>
-        <Merge Id='VCRuntime' DiskId='1' Language='1033'
-              SourceFile='$(var.VCRedistSrcDir)\microsoft.vcxx.crt.x64_msm.msm' />
-        <Merge Id='VCRuntimePolicy' DiskId='1' Language='1033'
-              SourceFile='$(var.VCRedistSrcDir)\policy.x.xx.microsoft.vcxx.crt.x64_msm.msm' />
+      <!-- Install VCRedist merge modules on Python 2. On Python 3,
+           vcruntimeXXX.dll is part of the install layout and gets picked up
+           as a regular file. -->
+      <?if $(var.PythonVersion) = "2" ?>
+        <?if $(var.Platform) = "x86" ?>
+          <Merge Id='VCRuntime' DiskId='1' Language='1033'
+                SourceFile='$(var.VCRedistSrcDir)\microsoft.vcxx.crt.x86_msm.msm' />
+          <Merge Id='VCRuntimePolicy' DiskId='1' Language='1033'
+                SourceFile='$(var.VCRedistSrcDir)\policy.x.xx.microsoft.vcxx.crt.x86_msm.msm' />
+        <?else?>
+          <Merge Id='VCRuntime' DiskId='1' Language='1033'
+                SourceFile='$(var.VCRedistSrcDir)\microsoft.vcxx.crt.x64_msm.msm' />
+          <Merge Id='VCRuntimePolicy' DiskId='1' Language='1033'
+                SourceFile='$(var.VCRedistSrcDir)\policy.x.xx.microsoft.vcxx.crt.x64_msm.msm' />
+        <?endif?>
       <?endif?>
     </Directory>
 
@@ -101,10 +106,14 @@
         <ComponentGroupRef Id="hg.group.ROOT" />
         <ComponentGroupRef Id="hg.group.defaultrc" />
         <ComponentGroupRef Id="hg.group.helptext" />
-        <ComponentGroupRef Id="hg.group.lib" />
+        <?ifdef MercurialHasLib?>
+          <ComponentGroupRef Id="hg.group.lib" />
+        <?endif?>
         <ComponentGroupRef Id="hg.group.templates" />
-        <MergeRef Id='VCRuntime' />
-        <MergeRef Id='VCRuntimePolicy' />
+        <?if $(var.PythonVersion) = "2" ?>
+          <MergeRef Id='VCRuntime' />
+          <MergeRef Id='VCRuntimePolicy' />
+        <?endif?>
       </Feature>
       <?ifdef MercurialExtraFeatures?>
         <?foreach EXTRAFEAT in $(var.MercurialExtraFeatures)?>
--- a/mercurial/patch.py	Thu Mar 05 17:55:05 2020 +0100
+++ b/mercurial/patch.py	Fri May 01 08:07:25 2020 -0700
@@ -2558,7 +2558,7 @@
                 fctx2 is not None
             ), b'fctx2 unexpectly None in diff hunks filtering'
             hunks = hunksfilterfn(fctx2, hunks)
-        text = b''.join(sum((list(hlines) for hrange, hlines in hunks), []))
+        text = b''.join(b''.join(hlines) for hrange, hlines in hunks)
         if hdr and (text or len(hdr) > 1):
             yield b'\n'.join(hdr) + b'\n'
         if text:
--- a/rust/hgcli/pyoxidizer.bzl	Thu Mar 05 17:55:05 2020 +0100
+++ b/rust/hgcli/pyoxidizer.bzl	Fri May 01 08:07:25 2020 -0700
@@ -1,13 +1,24 @@
 ROOT = CWD + "/../.."
 
-def make_exe():
-    dist = default_python_distribution()
+# Code to run in Python interpreter.
+RUN_CODE = "import hgdemandimport; hgdemandimport.enable(); from mercurial import dispatch; dispatch.run()"
+
+
+set_build_path(ROOT + "/build/pyoxidizer")
+
 
-    code = "import hgdemandimport; hgdemandimport.enable(); from mercurial import dispatch; dispatch.run()"
+def make_distribution():
+    return default_python_distribution()
+
 
+def make_distribution_windows():
+    return default_python_distribution(flavor="standalone_dynamic")
+
+
+def make_exe(dist):
     config = PythonInterpreterConfig(
         raw_allocator = "system",
-        run_eval = code,
+        run_eval = RUN_CODE,
         # We want to let the user load extensions from the file system
         filesystem_importer = True,
         # We need this to make resourceutil happy, since it looks for sys.frozen.
@@ -24,30 +35,65 @@
         extension_module_filter = "all",
     )
 
-    exe.add_python_resources(dist.pip_install([ROOT]))
+    # Add Mercurial to resources.
+    for resource in dist.pip_install(["--verbose", ROOT]):
+        # This is a bit wonky and worth explaining.
+        #
+        # Various parts of Mercurial don't yet support loading package
+        # resources via the ResourceReader interface. Or, not having
+        # file-based resources would be too inconvenient for users.
+        #
+        # So, for package resources, we package them both in the
+        # filesystem as well as in memory. If both are defined,
+        # PyOxidizer will prefer the in-memory location. So even
+        # if the filesystem file isn't packaged in the location
+        # specified here, we should never encounter an errors as the
+        # resource will always be available in memory.
+        if type(resource) == "PythonPackageResource":
+            exe.add_filesystem_relative_python_resource(".", resource)
+            exe.add_in_memory_python_resource(resource)
+        else:
+            exe.add_python_resource(resource)
+
+    # On Windows, we install extra packages for convenience.
+    if "windows" in BUILD_TARGET_TRIPLE:
+        exe.add_python_resources(
+            dist.pip_install(["-r", ROOT + "/contrib/packaging/requirements_win32.txt"])
+        )
 
     return exe
 
-def make_install(exe):
+
+def make_manifest(dist, exe):
     m = FileManifest()
-
-    # `hg` goes in root directory.
     m.add_python_resource(".", exe)
 
-    templates = glob(
-        include = [ROOT + "/mercurial/templates/**/*"],
-        strip_prefix = ROOT + "/mercurial/",
-    )
-    m.add_manifest(templates)
+    return m
 
-    return m
 
 def make_embedded_resources(exe):
     return exe.to_embedded_resources()
 
-register_target("exe", make_exe)
-register_target("app", make_install, depends = ["exe"], default = True)
-register_target("embedded", make_embedded_resources, depends = ["exe"], default_build_script = True)
+
+register_target("distribution_posix", make_distribution)
+register_target("distribution_windows", make_distribution_windows)
+
+register_target("exe_posix", make_exe, depends = ["distribution_posix"])
+register_target("exe_windows", make_exe, depends = ["distribution_windows"])
+
+register_target(
+    "app_posix",
+    make_manifest,
+    depends = ["distribution_posix", "exe_posix"],
+    default = "windows" not in BUILD_TARGET_TRIPLE,
+)
+register_target(
+    "app_windows",
+    make_manifest,
+    depends = ["distribution_windows", "exe_windows"],
+    default = "windows" in BUILD_TARGET_TRIPLE,
+)
+
 resolve_targets()
 
 # END OF COMMON USER-ADJUSTED SETTINGS.
@@ -55,5 +101,4 @@
 # Everything below this is typically managed by PyOxidizer and doesn't need
 # to be updated by people.
 
-PYOXIDIZER_VERSION = "0.7.0-pre"
-PYOXIDIZER_COMMIT = "c772a1379c3026314eda1c8ea244b86c0658951d"
+PYOXIDIZER_VERSION = "0.7.0"
--- a/tests/test-check-code.t	Thu Mar 05 17:55:05 2020 +0100
+++ b/tests/test-check-code.t	Fri May 01 08:07:25 2020 -0700
@@ -27,6 +27,7 @@
   Skipping contrib/packaging/hgpackaging/downloads.py it has no-che?k-code (glob)
   Skipping contrib/packaging/hgpackaging/inno.py it has no-che?k-code (glob)
   Skipping contrib/packaging/hgpackaging/py2exe.py it has no-che?k-code (glob)
+  Skipping contrib/packaging/hgpackaging/pyoxidizer.py it has no-che?k-code (glob)
   Skipping contrib/packaging/hgpackaging/util.py it has no-che?k-code (glob)
   Skipping contrib/packaging/hgpackaging/wix.py it has no-che?k-code (glob)
   Skipping i18n/polib.py it has no-che?k-code (glob)