diff --git a/contrib/packaging/hgpackaging/cli.py b/contrib/packaging/hgpackaging/cli.py --- a/contrib/packaging/hgpackaging/cli.py +++ b/contrib/packaging/hgpackaging/cli.py @@ -20,13 +20,7 @@ SOURCE_DIR = HERE.parent.parent.parent -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") - +def build_inno(pyoxidizer_target, iscc=None, version=None): if iscc: iscc = pathlib.Path(iscc) else: @@ -38,59 +32,30 @@ build_dir = SOURCE_DIR / "build" - 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, - ) + inno.build_with_pyoxidizer( + SOURCE_DIR, build_dir, pyoxidizer_target, iscc, version=version + ) def build_wix( + pyoxidizer_target, name=None, - pyoxidizer_target=None, - python=None, version=None, sign_sn=None, sign_cert=None, sign_password=None, sign_timestamp_url=None, - extra_packages_script=None, extra_wxs=None, extra_features=None, extra_pyoxidizer_vars=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") - kwargs = { "source_dir": SOURCE_DIR, "version": version, + "target_triple": pyoxidizer_target, + "extra_pyoxidizer_vars": extra_pyoxidizer_vars, } - if pyoxidizer_target: - fn = wix.build_installer_pyoxidizer - kwargs["target_triple"] = pyoxidizer_target - kwargs["extra_pyoxidizer_vars"] = extra_pyoxidizer_vars - 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( thing.split("=") for thing in extra_wxs.split(",") @@ -107,7 +72,7 @@ "timestamp_url": sign_timestamp_url, } - fn(**kwargs) + wix.build_installer_pyoxidizer(**kwargs) def get_parser(): @@ -119,9 +84,9 @@ sp.add_argument( "--pyoxidizer-target", choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"}, + required=True, 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", @@ -137,9 +102,9 @@ sp.add_argument( "--pyoxidizer-target", choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"}, + required=True, 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 " @@ -155,12 +120,6 @@ ) sp.add_argument("--version", help="Version string to use") sp.add_argument( - "--extra-packages-script", - help=( - "Script to execute to include extra packages in " "py2exe binary." - ), - ) - sp.add_argument( "--extra-wxs", help="CSV of path_to_wxs_file=working_dir_for_wxs_file" ) sp.add_argument( diff --git a/contrib/packaging/hgpackaging/downloads.py b/contrib/packaging/hgpackaging/downloads.py --- a/contrib/packaging/hgpackaging/downloads.py +++ b/contrib/packaging/hgpackaging/downloads.py @@ -25,48 +25,6 @@ 'size': 715086, 'sha256': '411f94974492fd2ecf52590cb05b1023530aec67e64154a88b1e4ebcd9c28588', }, - 'py2exe': { - 'url': 'https://versaweb.dl.sourceforge.net/project/py2exe/py2exe/0.6.9/py2exe-0.6.9.zip', - 'size': 149687, - 'sha256': '6bd383312e7d33eef2e43a5f236f9445e4f3e0f6b16333c6f183ed445c44ddbd', - 'version': '0.6.9', - }, - # The VC9 CRT merge modules aren't readily available on most systems because - # they are only installed as part of a full Visual Studio 2008 install. - # While we could potentially extract them from a Visual Studio 2008 - # installer, it is easier to just fetch them from a known URL. - 'vc9-crt-x86-msm': { - 'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/Microsoft_VC90_CRT_x86.msm', - 'size': 615424, - 'sha256': '837e887ef31b332feb58156f429389de345cb94504228bb9a523c25a9dd3d75e', - }, - 'vc9-crt-x86-msm-policy': { - 'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/policy_9_0_Microsoft_VC90_CRT_x86.msm', - 'size': 71168, - 'sha256': '3fbcf92e3801a0757f36c5e8d304e134a68d5cafd197a6df7734ae3e8825c940', - }, - 'vc9-crt-x64-msm': { - 'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/Microsoft_VC90_CRT_x86_x64.msm', - 'size': 662528, - 'sha256': '50d9639b5ad4844a2285269c7551bf5157ec636e32396ddcc6f7ec5bce487a7c', - }, - 'vc9-crt-x64-msm-policy': { - 'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/policy_9_0_Microsoft_VC90_CRT_x86_x64.msm', - 'size': 71168, - 'sha256': '0550ea1929b21239134ad3a678c944ba0f05f11087117b6cf0833e7110686486', - }, - 'virtualenv': { - 'url': 'https://files.pythonhosted.org/packages/37/db/89d6b043b22052109da35416abc3c397655e4bd3cff031446ba02b9654fa/virtualenv-16.4.3.tar.gz', - 'size': 3713208, - 'sha256': '984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39', - 'version': '16.4.3', - }, - 'wix': { - 'url': 'https://github.com/wixtoolset/wix3/releases/download/wix3111rtm/wix311-binaries.zip', - 'size': 34358269, - 'sha256': '37f0a533b0978a454efb5dc3bd3598becf9660aaf4287e55bf68ca6b527d051d', - 'version': '3.11.1', - }, } diff --git a/contrib/packaging/hgpackaging/inno.py b/contrib/packaging/hgpackaging/inno.py --- a/contrib/packaging/hgpackaging/inno.py +++ b/contrib/packaging/hgpackaging/inno.py @@ -14,29 +14,13 @@ import jinja2 -from .py2exe import ( - build_py2exe, - stage_install, -) from .pyoxidizer import create_pyoxidizer_install_layout from .util import ( - find_legacy_vc_runtime_files, normalize_windows_version, process_install_rules, read_version_py, ) -EXTRA_PACKAGES = { - 'dulwich', - 'keyring', - 'pygments', - 'win32ctypes', -} - -EXTRA_INCLUDES = { - '_curses', - '_curses_panel', -} EXTRA_INSTALL_RULES = [ ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'), @@ -47,80 +31,6 @@ } -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 using py2exe. - - Build files will be placed in ``build_dir``. - - py2exe's setup.py doesn't use setuptools. It doesn't have modern logic - for finding the Python 2.7 toolchain. So, we require the environment - to already be configured with an active toolchain. - """ - if not iscc_exe.exists(): - raise Exception('%s does not exist' % iscc_exe) - - vc_x64 = r'\x64' in os.environ.get('LIB', '') - arch = 'x64' if vc_x64 else 'x86' - inno_build_dir = build_dir / ('inno-py2exe-%s' % arch) - staging_dir = inno_build_dir / 'stage' - - requirements_txt = ( - source_dir / 'contrib' / 'packaging' / 'requirements-windows-py2.txt' - ) - - inno_build_dir.mkdir(parents=True, exist_ok=True) - - build_py2exe( - source_dir, - build_dir, - python_exe, - 'inno', - requirements_txt, - extra_packages=EXTRA_PACKAGES, - extra_includes=EXTRA_INCLUDES, - ) - - # Purge the staging directory for every build so packaging is - # pristine. - if staging_dir.exists(): - print('purging %s' % staging_dir) - shutil.rmtree(staging_dir) - - # Now assemble all the packaged files into the staging directory. - stage_install(source_dir, staging_dir) - - # We also install some extra files. - 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_legacy_vc_runtime_files(vc_x64): - if f.name.endswith('.manifest'): - basename = 'Microsoft.VC90.CRT.manifest' - else: - basename = f.name - - dest_path = staging_dir / basename - - 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, diff --git a/contrib/packaging/hgpackaging/py2exe.py b/contrib/packaging/hgpackaging/py2exe.py deleted file mode 100644 --- a/contrib/packaging/hgpackaging/py2exe.py +++ /dev/null @@ -1,248 +0,0 @@ -# py2exe.py - Functionality for performing py2exe builds. -# -# Copyright 2019 Gregory Szorc -# -# 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 subprocess - -from .downloads import download_entry -from .util import ( - extract_tar_to_directory, - extract_zip_to_directory, - process_install_rules, - python_exe_info, -) - - -STAGING_RULES = [ - ('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/'), - ('dist/hg.exe', './'), - ('dist/lib/*.dll', 'lib/'), - ('dist/lib/*.pyd', 'lib/'), - ('dist/lib/library.zip', 'lib/'), - ('dist/Microsoft.VC*.CRT.manifest', './'), - ('dist/msvc*.dll', './'), - ('dist/python*.dll', './'), - ('doc/*.html', 'doc/'), - ('doc/style.css', 'doc/'), - ('mercurial/helptext/**/*.txt', 'helptext/'), - ('mercurial/defaultrc/*.rc', 'defaultrc/'), - ('mercurial/locale/**/*', 'locale/'), - ('mercurial/templates/**/*', 'templates/'), - ('COPYING', 'Copying.txt'), -] - -# List of paths to exclude from the staging area. -STAGING_EXCLUDES = [ - 'doc/hg-ssh.8.html', -] - - -def build_py2exe( - source_dir: pathlib.Path, - build_dir: pathlib.Path, - python_exe: pathlib.Path, - build_name: str, - venv_requirements_txt: pathlib.Path, - extra_packages=None, - extra_excludes=None, - extra_dll_excludes=None, - extra_packages_script=None, - extra_includes=None, -): - """Build Mercurial with py2exe. - - Build files will be placed in ``build_dir``. - - py2exe's setup.py doesn't use setuptools. It doesn't have modern logic - for finding the Python 2.7 toolchain. So, we require the environment - to already be configured with an active toolchain. - """ - if 'VCINSTALLDIR' not in os.environ: - raise Exception( - 'not running from a Visual C++ build environment; ' - 'execute the "Visual C++ Command Prompt" ' - 'application shortcut or a vcsvarsall.bat file' - ) - - # Identity x86/x64 and validate the environment matches the Python - # architecture. - vc_x64 = r'\x64' in os.environ['LIB'] - - py_info = python_exe_info(python_exe) - - if vc_x64: - if py_info['arch'] != '64bit': - raise Exception( - 'architecture mismatch: Visual C++ environment ' - 'is configured for 64-bit but Python is 32-bit' - ) - else: - if py_info['arch'] != '32bit': - raise Exception( - 'architecture mismatch: Visual C++ environment ' - 'is configured for 32-bit but Python is 64-bit' - ) - - if py_info['py3']: - raise Exception('Only Python 2 is currently supported') - - build_dir.mkdir(exist_ok=True) - - gettext_pkg, gettext_entry = download_entry('gettext', build_dir) - gettext_dep_pkg = download_entry('gettext-dep', build_dir)[0] - virtualenv_pkg, virtualenv_entry = download_entry('virtualenv', build_dir) - py2exe_pkg, py2exe_entry = download_entry('py2exe', build_dir) - - venv_path = build_dir / ( - 'venv-%s-%s' % (build_name, 'x64' if vc_x64 else 'x86') - ) - - 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) - - # This assumes Python 2. We don't need virtualenv on Python 3. - virtualenv_src_path = build_dir / ( - 'virtualenv-%s' % virtualenv_entry['version'] - ) - virtualenv_py = virtualenv_src_path / 'virtualenv.py' - - if not virtualenv_src_path.exists(): - extract_tar_to_directory(virtualenv_pkg, build_dir) - - py2exe_source_path = build_dir / ('py2exe-%s' % py2exe_entry['version']) - - if not py2exe_source_path.exists(): - extract_zip_to_directory(py2exe_pkg, build_dir) - - if not venv_path.exists(): - print('creating virtualenv with dependencies') - subprocess.run( - [str(python_exe), str(virtualenv_py), str(venv_path)], check=True - ) - - venv_python = venv_path / 'Scripts' / 'python.exe' - venv_pip = venv_path / 'Scripts' / 'pip.exe' - - subprocess.run( - [str(venv_pip), 'install', '-r', str(venv_requirements_txt)], check=True - ) - - # Force distutils to use VC++ settings from environment, which was - # validated above. - env = dict(os.environ) - env['DISTUTILS_USE_SDK'] = '1' - env['MSSdk'] = '1' - - if extra_packages_script: - more_packages = set( - subprocess.check_output(extra_packages_script, cwd=build_dir) - .split(b'\0')[-1] - .strip() - .decode('utf-8') - .splitlines() - ) - if more_packages: - if not extra_packages: - extra_packages = more_packages - else: - extra_packages |= more_packages - - if extra_packages: - env['HG_PY2EXE_EXTRA_PACKAGES'] = ' '.join(sorted(extra_packages)) - hgext3rd_extras = sorted( - e for e in extra_packages if e.startswith('hgext3rd.') - ) - if hgext3rd_extras: - env['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'] = ' '.join(hgext3rd_extras) - if extra_includes: - env['HG_PY2EXE_EXTRA_INCLUDES'] = ' '.join(sorted(extra_includes)) - if extra_excludes: - env['HG_PY2EXE_EXTRA_EXCLUDES'] = ' '.join(sorted(extra_excludes)) - if extra_dll_excludes: - env['HG_PY2EXE_EXTRA_DLL_EXCLUDES'] = ' '.join( - sorted(extra_dll_excludes) - ) - - py2exe_py_path = venv_path / 'Lib' / 'site-packages' / 'py2exe' - if not py2exe_py_path.exists(): - print('building py2exe') - subprocess.run( - [str(venv_python), 'setup.py', 'install'], - cwd=py2exe_source_path, - env=env, - check=True, - ) - - # Register location of msgfmt and other binaries. - env['PATH'] = '%s%s%s' % ( - env['PATH'], - os.pathsep, - str(gettext_root / 'bin'), - ) - - print('building Mercurial') - subprocess.run( - [str(venv_python), 'setup.py', 'py2exe', 'build_doc', '--html'], - cwd=str(source_dir), - env=env, - check=True, - ) - - -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. - """ - 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. - with (staging_dir / 'defaultrc' / 'editor.rc').open( - 'w', encoding='utf-8' - ) as fh: - fh.write('[ui]\neditor = notepad\n') - - # Purge any files we don't want to be there. - for f in STAGING_EXCLUDES: - p = staging_dir / f - if p.exists(): - print('removing %s' % p) - p.unlink() diff --git a/contrib/packaging/hgpackaging/util.py b/contrib/packaging/hgpackaging/util.py --- a/contrib/packaging/hgpackaging/util.py +++ b/contrib/packaging/hgpackaging/util.py @@ -7,23 +7,15 @@ # no-check-code because Python 3 native. -import distutils.version -import getpass import glob import os import pathlib import re import shutil import subprocess -import tarfile import zipfile -def extract_tar_to_directory(source: pathlib.Path, dest: pathlib.Path): - with tarfile.open(source, 'r') as tf: - tf.extractall(dest) - - def extract_zip_to_directory(source: pathlib.Path, dest: pathlib.Path): with zipfile.ZipFile(source, 'r') as zf: zf.extractall(dest) @@ -81,59 +73,6 @@ 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' - - prefix = 'amd64' if x64 else 'x86' - - candidates = sorted( - p - for p in os.listdir(winsxs) - if p.lower().startswith('%s_microsoft.vc90.crt_' % prefix) - ) - - for p in candidates: - print('found candidate VC runtime: %s' % p) - - # Take the newest version. - version = candidates[-1] - - d = winsxs / version - - return [ - d / 'msvcm90.dll', - d / 'msvcp90.dll', - d / 'msvcr90.dll', - winsxs / 'Manifests' / ('%s.manifest' % version), - ] - - -def windows_10_sdk_info(): - """Resolves information about the Windows 10 SDK.""" - - base = pathlib.Path(os.environ['ProgramFiles(x86)']) / 'Windows Kits' / '10' - - if not base.is_dir(): - raise Exception('unable to find Windows 10 SDK at %s' % base) - - # Find the latest version. - bin_base = base / 'bin' - - versions = [v for v in os.listdir(bin_base) if v.startswith('10.')] - version = sorted(versions, reverse=True)[0] - - bin_version = bin_base / version - - return { - 'root': base, - 'version': version, - 'bin_root': bin_version, - 'bin_x86': bin_version / 'x86', - 'bin_x64': bin_version / 'x64', - } - - def normalize_windows_version(version): """Normalize Mercurial version string so WiX/Inno accepts it. @@ -194,93 +133,6 @@ return '.'.join('%d' % x for x in versions[0:4]) -def find_signtool(): - """Find signtool.exe from the Windows SDK.""" - sdk = windows_10_sdk_info() - - for key in ('bin_x64', 'bin_x86'): - p = sdk[key] / 'signtool.exe' - - if p.exists(): - return p - - raise Exception('could not find signtool.exe in Windows 10 SDK') - - -def sign_with_signtool( - file_path, - description, - subject_name=None, - cert_path=None, - cert_password=None, - timestamp_url=None, -): - """Digitally sign a file with signtool.exe. - - ``file_path`` is file to sign. - ``description`` is text that goes in the signature. - - The signing certificate can be specified by ``cert_path`` or - ``subject_name``. These correspond to the ``/f`` and ``/n`` arguments - to signtool.exe, respectively. - - The certificate password can be specified via ``cert_password``. If - not provided, you will be prompted for the password. - - ``timestamp_url`` is the URL of a RFC 3161 timestamp server (``/tr`` - argument to signtool.exe). - """ - if cert_path and subject_name: - raise ValueError('cannot specify both cert_path and subject_name') - - while cert_path and not cert_password: - cert_password = getpass.getpass('password for %s: ' % cert_path) - - args = [ - str(find_signtool()), - 'sign', - '/v', - '/fd', - 'sha256', - '/d', - description, - ] - - if cert_path: - args.extend(['/f', str(cert_path), '/p', cert_password]) - elif subject_name: - args.extend(['/n', subject_name]) - - if timestamp_url: - args.extend(['/tr', timestamp_url, '/td', 'sha256']) - - args.append(str(file_path)) - - print('signing %s' % file_path) - subprocess.run(args, check=True) - - -PRINT_PYTHON_INFO = ''' -import platform; print("%s:%s" % (platform.architecture()[0], platform.python_version())) -'''.strip() - - -def python_exe_info(python_exe: pathlib.Path): - """Obtain information about a Python executable.""" - - res = subprocess.check_output([str(python_exe), '-c', PRINT_PYTHON_INFO]) - - arch, version = res.decode('utf-8').split(':') - - version = distutils.version.LooseVersion(version) - - return { - 'arch': arch, - 'version': version, - 'py3': version >= distutils.version.LooseVersion('3'), - } - - def process_install_rules( rules: list, source_dir: pathlib.Path, dest_dir: pathlib.Path ): 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,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( - '' - '' - '' - ) - - # 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( - '/', '.' - ).replace('-', '_') - - 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).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 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 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, - } diff --git a/contrib/packaging/requirements-windows-py2.txt b/contrib/packaging/requirements-windows-py2.txt deleted file mode 100644 --- a/contrib/packaging/requirements-windows-py2.txt +++ /dev/null @@ -1,59 +0,0 @@ -# -# This file is autogenerated by pip-compile -# To update, run: -# -# pip-compile --generate-hashes --output-file=contrib/packaging/requirements-windows-py2.txt contrib/packaging/requirements-windows.txt.in -# -certifi==2021.5.30 \ - --hash=sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee \ - --hash=sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8 \ - # via dulwich -configparser==4.0.2 \ - --hash=sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c \ - --hash=sha256:c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df \ - # via entrypoints -docutils==0.16 \ - --hash=sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af \ - --hash=sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc \ - # via -r contrib/packaging/requirements-windows.txt.in -dulwich==0.19.16 ; python_version <= "2.7" \ - --hash=sha256:10699277c6268d0c16febe141a5b1c1a6e9744f3144c2d2de1706f4b1adafe63 \ - --hash=sha256:267160904e9a1cb6c248c5efc53597a35d038ecc6f60bdc4546b3053bed11982 \ - --hash=sha256:4e3aba5e4844e7c700721c1fc696987ea820ee3528a03604dc4e74eff4196826 \ - --hash=sha256:60bb2c2c92f5025c1b53a556304008f0f624c98ae36f22d870e056b2d4236c11 \ - --hash=sha256:dddae02d372fc3b5cfb0046d0f62246ef281fa0c088df7601ab5916607add94b \ - --hash=sha256:f00d132082b8fcc2eb0d722abc773d4aeb5558c1475d7edd1f0f571146c29db9 \ - --hash=sha256:f74561c448bfb6f04c07de731c1181ae4280017f759b0bb04fa5770aa84ca850 \ - # via -r contrib/packaging/requirements-windows.txt.in -entrypoints==0.3 \ - --hash=sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19 \ - --hash=sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451 \ - # via keyring -keyring==18.0.1 \ - --hash=sha256:67d6cc0132bd77922725fae9f18366bb314fd8f95ff4d323a4df41890a96a838 \ - --hash=sha256:7b29ebfcf8678c4da531b2478a912eea01e80007e5ddca9ee0c7038cb3489ec6 \ - # via -r contrib/packaging/requirements-windows.txt.in -pygments==2.5.2 \ - --hash=sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b \ - --hash=sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe \ - # via -r contrib/packaging/requirements-windows.txt.in -pywin32-ctypes==0.2.0 \ - --hash=sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942 \ - --hash=sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98 \ - # via -r contrib/packaging/requirements-windows.txt.in, keyring -urllib3==1.25.11 \ - --hash=sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2 \ - --hash=sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e \ - # via dulwich -windows-curses==2.1.0 \ - --hash=sha256:261fde5680d1ce4ce116908996b9a3cfb0ffb03ea68d42240f62b56a9fa6af2c \ - --hash=sha256:66034dc9a705d87308cc9ea90836f4ee60008a1d5e2c1d34ace627f60268158b \ - --hash=sha256:669caad3ae16faf2d201d7ab3b8af418a2fd074d8a39d60ca26f3acb34b6afe5 \ - --hash=sha256:73bd3eebccfda55330783f165151de115bfa238d1332f0b2e224b550d6187840 \ - --hash=sha256:89a6d973f88cfe49b41ea80164dcbec209d296e0cec34a02002578b0bf464a64 \ - --hash=sha256:8ba7c000d7ffa5452bbd0966b96e69261e4f117ebe510aeb8771a9650197b7f0 \ - --hash=sha256:97084c6b37b1534f6a28a514d521dfae402f77dcbad42b14ee32e8d5bdc13648 \ - --hash=sha256:9e474a181f96d60429a4766145628264e60b72e7715876f9135aeb2e842f9433 \ - --hash=sha256:cfe64c30807c146ef8d094412f90f2a2c81ad6aefff3ebfe8e37aabe2f801303 \ - --hash=sha256:ff8c67f74b88944d99fa9d22971c05c335bc74f149120f0a69340c2c3a595497 \ - # via -r contrib/packaging/requirements-windows.txt.in diff --git a/tests/test-check-code.t b/tests/test-check-code.t --- a/tests/test-check-code.t +++ b/tests/test-check-code.t @@ -27,7 +27,6 @@ Skipping contrib/packaging/hgpackaging/cli.py it has no-che?k-code (glob) 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)