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 @@ -1,175 +1,175 @@ -# downloads.py - Code for downloading dependencies. -# -# 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 gzip -import hashlib -import pathlib -import urllib.request - - -DOWNLOADS = { - 'gettext': { - 'url': 'https://versaweb.dl.sourceforge.net/project/gnuwin32/gettext/0.14.4/gettext-0.14.4-bin.zip', - 'size': 1606131, - 'sha256': '60b9ef26bc5cceef036f0424e542106cf158352b2677f43a01affd6d82a1d641', - 'version': '0.14.4', - }, - 'gettext-dep': { - 'url': 'https://versaweb.dl.sourceforge.net/project/gnuwin32/gettext/0.14.4/gettext-0.14.4-dep.zip', - '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', - }, -} - - -def hash_path(p: pathlib.Path): - h = hashlib.sha256() - - with p.open('rb') as fh: - while True: - chunk = fh.read(65536) - if not chunk: - break - - h.update(chunk) - - return h.hexdigest() - - -class IntegrityError(Exception): - """Represents an integrity error when downloading a URL.""" - - -def secure_download_stream(url, size, sha256): - """Securely download a URL to a stream of chunks. - - If the integrity of the download fails, an IntegrityError is - raised. - """ - h = hashlib.sha256() - length = 0 - - with urllib.request.urlopen(url) as fh: - if not url.endswith('.gz') and fh.info().get('Content-Encoding') == 'gzip': - fh = gzip.GzipFile(fileobj=fh) - - while True: - chunk = fh.read(65536) - if not chunk: - break - - h.update(chunk) - length += len(chunk) - - yield chunk - - digest = h.hexdigest() - - if length != size: - raise IntegrityError('size mismatch on %s: wanted %d; got %d' % ( - url, size, length)) - - if digest != sha256: - raise IntegrityError('sha256 mismatch on %s: wanted %s; got %s' % ( - url, sha256, digest)) - - -def download_to_path(url: str, path: pathlib.Path, size: int, sha256: str): - """Download a URL to a filesystem path, possibly with verification.""" - - # We download to a temporary file and rename at the end so there's - # no chance of the final file being partially written or containing - # bad data. - print('downloading %s to %s' % (url, path)) - - if path.exists(): - good = True - - if path.stat().st_size != size: - print('existing file size is wrong; removing') - good = False - - if good: - if hash_path(path) != sha256: - print('existing file hash is wrong; removing') - good = False - - if good: - print('%s exists and passes integrity checks' % path) - return - - path.unlink() - - tmp = path.with_name('%s.tmp' % path.name) - - try: - with tmp.open('wb') as fh: - for chunk in secure_download_stream(url, size, sha256): - fh.write(chunk) - except IntegrityError: - tmp.unlink() - raise - - tmp.rename(path) - print('successfully downloaded %s' % url) - - -def download_entry(name: dict, dest_path: pathlib.Path, local_name=None) -> pathlib.Path: - entry = DOWNLOADS[name] - - url = entry['url'] - - local_name = local_name or url[url.rindex('/') + 1:] - - local_path = dest_path / local_name - download_to_path(url, local_path, entry['size'], entry['sha256']) - - return local_path, entry +# downloads.py - Code for downloading dependencies. +# +# 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 gzip +import hashlib +import pathlib +import urllib.request + + +DOWNLOADS = { + 'gettext': { + 'url': 'https://versaweb.dl.sourceforge.net/project/gnuwin32/gettext/0.14.4/gettext-0.14.4-bin.zip', + 'size': 1606131, + 'sha256': '60b9ef26bc5cceef036f0424e542106cf158352b2677f43a01affd6d82a1d641', + 'version': '0.14.4', + }, + 'gettext-dep': { + 'url': 'https://versaweb.dl.sourceforge.net/project/gnuwin32/gettext/0.14.4/gettext-0.14.4-dep.zip', + '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', + }, +} + + +def hash_path(p: pathlib.Path): + h = hashlib.sha256() + + with p.open('rb') as fh: + while True: + chunk = fh.read(65536) + if not chunk: + break + + h.update(chunk) + + return h.hexdigest() + + +class IntegrityError(Exception): + """Represents an integrity error when downloading a URL.""" + + +def secure_download_stream(url, size, sha256): + """Securely download a URL to a stream of chunks. + + If the integrity of the download fails, an IntegrityError is + raised. + """ + h = hashlib.sha256() + length = 0 + + with urllib.request.urlopen(url) as fh: + if not url.endswith('.gz') and fh.info().get('Content-Encoding') == 'gzip': + fh = gzip.GzipFile(fileobj=fh) + + while True: + chunk = fh.read(65536) + if not chunk: + break + + h.update(chunk) + length += len(chunk) + + yield chunk + + digest = h.hexdigest() + + if length != size: + raise IntegrityError('size mismatch on %s: wanted %d; got %d' % ( + url, size, length)) + + if digest != sha256: + raise IntegrityError('sha256 mismatch on %s: wanted %s; got %s' % ( + url, sha256, digest)) + + +def download_to_path(url: str, path: pathlib.Path, size: int, sha256: str): + """Download a URL to a filesystem path, possibly with verification.""" + + # We download to a temporary file and rename at the end so there's + # no chance of the final file being partially written or containing + # bad data. + print('downloading %s to %s' % (url, path)) + + if path.exists(): + good = True + + if path.stat().st_size != size: + print('existing file size is wrong; removing') + good = False + + if good: + if hash_path(path) != sha256: + print('existing file hash is wrong; removing') + good = False + + if good: + print('%s exists and passes integrity checks' % path) + return + + path.unlink() + + tmp = path.with_name('%s.tmp' % path.name) + + try: + with tmp.open('wb') as fh: + for chunk in secure_download_stream(url, size, sha256): + fh.write(chunk) + except IntegrityError: + tmp.unlink() + raise + + tmp.rename(path) + print('successfully downloaded %s' % url) + + +def download_entry(name: dict, dest_path: pathlib.Path, local_name=None) -> pathlib.Path: + entry = DOWNLOADS[name] + + url = entry['url'] + + local_name = local_name or url[url.rindex('/') + 1:] + + local_path = dest_path / local_name + download_to_path(url, local_path, entry['size'], entry['sha256']) + + return local_path, entry 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 @@ -1,157 +1,157 @@ -# util.py - Common packaging utility code. -# -# 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 distutils.version -import getpass -import os -import pathlib -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) - - -def find_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 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.run( - [str(python_exe), '-c', PRINT_PYTHON_INFO], - capture_output=True, check=True) - - arch, version = res.stdout.decode('utf-8').split(':') - - version = distutils.version.LooseVersion(version) - - return { - 'arch': arch, - 'version': version, - 'py3': version >= distutils.version.LooseVersion('3'), - } +# util.py - Common packaging utility code. +# +# 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 distutils.version +import getpass +import os +import pathlib +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) + + +def find_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 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.run( + [str(python_exe), '-c', PRINT_PYTHON_INFO], + capture_output=True, check=True) + + arch, version = res.stdout.decode('utf-8').split(':') + + version = distutils.version.LooseVersion(version) + + return { + 'arch': arch, + 'version': version, + 'py3': version >= distutils.version.LooseVersion('3'), + } 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 @@ -1,239 +1,239 @@ -# wix.py - WiX installer functionality -# -# 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 re -import subprocess - -from .downloads import ( - download_entry, -) -from .py2exe import ( - build_py2exe, -) -from .util import ( - extract_zip_to_directory, - sign_with_signtool, -) - - -SUPPORT_WXS = [ - ('contrib.wxs', r'contrib'), - ('dist.wxs', r'dist'), - ('doc.wxs', r'doc'), - ('help.wxs', r'mercurial\help'), - ('i18n.wxs', r'i18n'), - ('locale.wxs', r'mercurial\locale'), - ('templates.wxs', r'mercurial\templates'), -] - - -EXTRA_PACKAGES = { - 'distutils', - 'pygments', -} - - -def find_version(source_dir: pathlib.Path): - version_py = source_dir / 'mercurial' / '__version__.py' - - with version_py.open('r', encoding='utf-8') as fh: - source = fh.read().strip() - - m = re.search('version = b"(.*)"', source) - return m.group(1) - - -def normalize_version(version): - """Normalize Mercurial version string so WiX accepts it. - - Version strings have to be numeric X.Y.Z. - """ - - if '+' in version: - version, extra = version.split('+', 1) - else: - extra = None - - # 4.9rc0 - if version[:-1].endswith('rc'): - version = version[:-3] - - versions = [int(v) for v in version.split('.')] - while len(versions) < 3: - versions.append(0) - - major, minor, build = versions[:3] - - if extra: - # -+ - build = int(extra.split('-')[0]) - - return '.'.join('%d' % x for x in (major, minor, build)) - - -def ensure_vc90_merge_modules(build_dir): - x86 = ( - download_entry('vc9-crt-x86-msm', build_dir, - local_name='microsoft.vcxx.crt.x86_msm.msm')[0], - download_entry('vc9-crt-x86-msm-policy', build_dir, - local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm')[0] - ) - - x64 = ( - download_entry('vc9-crt-x64-msm', build_dir, - local_name='microsoft.vcxx.crt.x64_msm.msm')[0], - download_entry('vc9-crt-x64-msm-policy', build_dir, - local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm')[0] - ) - return { - 'x86': x86, - 'x64': x64, - } - - -def run_candle(wix, cwd, wxs, source_dir, defines=None): - args = [ - str(wix / 'candle.exe'), - '-nologo', - str(wxs), - '-dSourceDir=%s' % source_dir, - ] - - if defines: - args.extend('-d%s=%s' % define for define in sorted(defines.items())) - - subprocess.run(args, cwd=str(cwd), check=True) - - -def make_post_build_signing_fn(name, subject_name=None, cert_path=None, - cert_password=None, timestamp_url=None): - """Create a callable that will use signtool to sign hg.exe.""" - - def post_build_sign(source_dir, build_dir, dist_dir, version): - description = '%s %s' % (name, version) - - sign_with_signtool(dist_dir / 'hg.exe', description, - subject_name=subject_name, cert_path=cert_path, - cert_password=cert_password, - timestamp_url=timestamp_url) - - return post_build_sign - - -def build_installer(source_dir: pathlib.Path, python_exe: pathlib.Path, - msi_name='mercurial', version=None, post_build_fn=None): - """Build a WiX MSI installer. - - ``source_dir`` is the path to the Mercurial source tree to use. - ``arch`` is the target architecture. either ``x86`` or ``x64``. - ``python_exe`` is the path to the Python executable to use/bundle. - ``version`` is the Mercurial version string. If not defined, - ``mercurial/__version__.py`` will be consulted. - ``post_build_fn`` is a callable that will be called after building - Mercurial but before invoking WiX. It can be used to e.g. facilitate - signing. It is passed the paths to the Mercurial source, build, and - dist directories and the resolved Mercurial version. - """ - arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86' - - hg_build_dir = source_dir / 'build' - dist_dir = source_dir / 'dist' - - requirements_txt = (source_dir / 'contrib' / 'packaging' / - 'wix' / 'requirements.txt') - - build_py2exe(source_dir, hg_build_dir, - python_exe, 'wix', requirements_txt, - extra_packages=EXTRA_PACKAGES) - - version = version or normalize_version(find_version(source_dir)) - print('using version string: %s' % version) - - if post_build_fn: - post_build_fn(source_dir, hg_build_dir, dist_dir, version) - - build_dir = hg_build_dir / ('wix-%s' % arch) - - build_dir.mkdir(exist_ok=True) - - wix_pkg, wix_entry = download_entry('wix', hg_build_dir) - wix_path = hg_build_dir / ('wix-%s' % wix_entry['version']) - - if not wix_path.exists(): - extract_zip_to_directory(wix_pkg, wix_path) - - ensure_vc90_merge_modules(hg_build_dir) - - source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir)) - - defines = {'Platform': arch} - - for wxs, rel_path in SUPPORT_WXS: - wxs = source_dir / 'contrib' / 'packaging' / 'wix' / wxs - wxs_source_dir = source_dir / rel_path - run_candle(wix_path, build_dir, wxs, wxs_source_dir, defines=defines) - - source = source_dir / 'contrib' / 'packaging' / 'wix' / 'mercurial.wxs' - defines['Version'] = version - defines['Comments'] = 'Installs Mercurial version %s' % version - defines['VCRedistSrcDir'] = str(hg_build_dir) - - run_candle(wix_path, build_dir, source, source_build_rel, defines=defines) - - msi_path = source_dir / 'dist' / ( - '%s-%s-%s.msi' % (msi_name, version, arch)) - - args = [ - str(wix_path / 'light.exe'), - '-nologo', - '-ext', 'WixUIExtension', - '-sw1076', - '-spdb', - '-o', str(msi_path), - ] - - for source, rel_path in SUPPORT_WXS: - assert source.endswith('.wxs') - args.append(str(build_dir / ('%s.wixobj' % source[:-4]))) - - args.append(str(build_dir / 'mercurial.wixobj')) - - subprocess.run(args, cwd=str(source_dir), check=True) - - print('%s created' % msi_path) - - return { - 'msi_path': msi_path, - } - - -def build_signed_installer(source_dir: pathlib.Path, python_exe: pathlib.Path, - name: str, version=None, subject_name=None, - cert_path=None, cert_password=None, - timestamp_url=None): - """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) - - 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) +# wix.py - WiX installer functionality +# +# 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 re +import subprocess + +from .downloads import ( + download_entry, +) +from .py2exe import ( + build_py2exe, +) +from .util import ( + extract_zip_to_directory, + sign_with_signtool, +) + + +SUPPORT_WXS = [ + ('contrib.wxs', r'contrib'), + ('dist.wxs', r'dist'), + ('doc.wxs', r'doc'), + ('help.wxs', r'mercurial\help'), + ('i18n.wxs', r'i18n'), + ('locale.wxs', r'mercurial\locale'), + ('templates.wxs', r'mercurial\templates'), +] + + +EXTRA_PACKAGES = { + 'distutils', + 'pygments', +} + + +def find_version(source_dir: pathlib.Path): + version_py = source_dir / 'mercurial' / '__version__.py' + + with version_py.open('r', encoding='utf-8') as fh: + source = fh.read().strip() + + m = re.search('version = b"(.*)"', source) + return m.group(1) + + +def normalize_version(version): + """Normalize Mercurial version string so WiX accepts it. + + Version strings have to be numeric X.Y.Z. + """ + + if '+' in version: + version, extra = version.split('+', 1) + else: + extra = None + + # 4.9rc0 + if version[:-1].endswith('rc'): + version = version[:-3] + + versions = [int(v) for v in version.split('.')] + while len(versions) < 3: + versions.append(0) + + major, minor, build = versions[:3] + + if extra: + # -+ + build = int(extra.split('-')[0]) + + return '.'.join('%d' % x for x in (major, minor, build)) + + +def ensure_vc90_merge_modules(build_dir): + x86 = ( + download_entry('vc9-crt-x86-msm', build_dir, + local_name='microsoft.vcxx.crt.x86_msm.msm')[0], + download_entry('vc9-crt-x86-msm-policy', build_dir, + local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm')[0] + ) + + x64 = ( + download_entry('vc9-crt-x64-msm', build_dir, + local_name='microsoft.vcxx.crt.x64_msm.msm')[0], + download_entry('vc9-crt-x64-msm-policy', build_dir, + local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm')[0] + ) + return { + 'x86': x86, + 'x64': x64, + } + + +def run_candle(wix, cwd, wxs, source_dir, defines=None): + args = [ + str(wix / 'candle.exe'), + '-nologo', + str(wxs), + '-dSourceDir=%s' % source_dir, + ] + + if defines: + args.extend('-d%s=%s' % define for define in sorted(defines.items())) + + subprocess.run(args, cwd=str(cwd), check=True) + + +def make_post_build_signing_fn(name, subject_name=None, cert_path=None, + cert_password=None, timestamp_url=None): + """Create a callable that will use signtool to sign hg.exe.""" + + def post_build_sign(source_dir, build_dir, dist_dir, version): + description = '%s %s' % (name, version) + + sign_with_signtool(dist_dir / 'hg.exe', description, + subject_name=subject_name, cert_path=cert_path, + cert_password=cert_password, + timestamp_url=timestamp_url) + + return post_build_sign + + +def build_installer(source_dir: pathlib.Path, python_exe: pathlib.Path, + msi_name='mercurial', version=None, post_build_fn=None): + """Build a WiX MSI installer. + + ``source_dir`` is the path to the Mercurial source tree to use. + ``arch`` is the target architecture. either ``x86`` or ``x64``. + ``python_exe`` is the path to the Python executable to use/bundle. + ``version`` is the Mercurial version string. If not defined, + ``mercurial/__version__.py`` will be consulted. + ``post_build_fn`` is a callable that will be called after building + Mercurial but before invoking WiX. It can be used to e.g. facilitate + signing. It is passed the paths to the Mercurial source, build, and + dist directories and the resolved Mercurial version. + """ + arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86' + + hg_build_dir = source_dir / 'build' + dist_dir = source_dir / 'dist' + + requirements_txt = (source_dir / 'contrib' / 'packaging' / + 'wix' / 'requirements.txt') + + build_py2exe(source_dir, hg_build_dir, + python_exe, 'wix', requirements_txt, + extra_packages=EXTRA_PACKAGES) + + version = version or normalize_version(find_version(source_dir)) + print('using version string: %s' % version) + + if post_build_fn: + post_build_fn(source_dir, hg_build_dir, dist_dir, version) + + build_dir = hg_build_dir / ('wix-%s' % arch) + + build_dir.mkdir(exist_ok=True) + + wix_pkg, wix_entry = download_entry('wix', hg_build_dir) + wix_path = hg_build_dir / ('wix-%s' % wix_entry['version']) + + if not wix_path.exists(): + extract_zip_to_directory(wix_pkg, wix_path) + + ensure_vc90_merge_modules(hg_build_dir) + + source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir)) + + defines = {'Platform': arch} + + for wxs, rel_path in SUPPORT_WXS: + wxs = source_dir / 'contrib' / 'packaging' / 'wix' / wxs + wxs_source_dir = source_dir / rel_path + run_candle(wix_path, build_dir, wxs, wxs_source_dir, defines=defines) + + source = source_dir / 'contrib' / 'packaging' / 'wix' / 'mercurial.wxs' + defines['Version'] = version + defines['Comments'] = 'Installs Mercurial version %s' % version + defines['VCRedistSrcDir'] = str(hg_build_dir) + + run_candle(wix_path, build_dir, source, source_build_rel, defines=defines) + + msi_path = source_dir / 'dist' / ( + '%s-%s-%s.msi' % (msi_name, version, arch)) + + args = [ + str(wix_path / 'light.exe'), + '-nologo', + '-ext', 'WixUIExtension', + '-sw1076', + '-spdb', + '-o', str(msi_path), + ] + + for source, rel_path in SUPPORT_WXS: + assert source.endswith('.wxs') + args.append(str(build_dir / ('%s.wixobj' % source[:-4]))) + + args.append(str(build_dir / 'mercurial.wixobj')) + + subprocess.run(args, cwd=str(source_dir), check=True) + + print('%s created' % msi_path) + + return { + 'msi_path': msi_path, + } + + +def build_signed_installer(source_dir: pathlib.Path, python_exe: pathlib.Path, + name: str, version=None, subject_name=None, + cert_path=None, cert_password=None, + timestamp_url=None): + """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) + + 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)