view contrib/packaging/inno/build.py @ 41854:7a1433e90482

inno: stop shipping pywin32 Ancient versions of Mercurial relied on pywin32 and I suspect that's why we have this dependency. We also ship the "keyring" package, which has a dependency on "pywin32-ctypes" (providing the "win32ctypes" package). This is a stripped down version of pywin32 that doesn't have as many dependencies. Since we don't have a dependency on pywin32 and since pywin32 is a bit annoying to package, let's get rid of it. With this change, py2exe no longers picks up DLL dependencies on various UCRT DLLs (because we no longer have a .pyd file beloning to pywin32 which was pulling them in). So, we were able to remove code in support of the UCRT DLLs. .. bc:: The Windows Inno installers no longer ship the pywin32 package. This package was being bundled for historical reasons. Mercurial stopped using pywin32 several years ago and the disappearance of this package should not have any meaningful impact. Differential Revision: https://phab.mercurial-scm.org/D6067
author Gregory Szorc <gregory.szorc@gmail.com>
date Sun, 03 Mar 2019 15:53:27 -0800
parents d7dc4ac1ff84
children 9da97f49d4f4
line wrap: on
line source

#!/usr/bin/env python3
# build.py - Inno installer build script.
#
# Copyright 2019 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.

# This script automates the building of the Inno MSI installer for Mercurial.

# no-check-code because Python 3 native.

import argparse
import os
import pathlib
import shutil
import subprocess
import sys
import tempfile


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',
    },
    'virtualenv': {
        'url': 'https://files.pythonhosted.org/packages/37/db/89d6b043b22052109da35416abc3c397655e4bd3cff031446ba02b9654fa/virtualenv-16.4.3.tar.gz',
        'size': 3713208,
        'sha256': '984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39',
        'version': '16.4.3',
    },
}


PRINT_PYTHON_INFO = '''
import platform, sys; print("%s:%d" % (platform.architecture()[0], sys.version_info[0]))
'''.strip()


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 build(source_dir: pathlib.Path, build_dir: pathlib.Path,
          python_exe: pathlib.Path, iscc_exe: pathlib.Path,
          version=None):
    """Build the Inno installer.

    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.
    """
    from packagingutil import (
        download_entry,
        extract_tar_to_directory,
        extract_zip_to_directory,
    )

    if not iscc.exists():
        raise Exception('%s does not exist' % iscc)

    if 'VCINSTALLDIR' not in os.environ:
        raise Exception('not running from a Visual C++ build environment; '
                        'execute the "Visual C++ <version> 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']

    res = subprocess.run(
        [str(python_exe), '-c', PRINT_PYTHON_INFO],
        capture_output=True, check=True)

    py_arch, py_version = res.stdout.decode('utf-8').split(':')
    py_version = int(py_version)

    if vc_x64:
        if py_arch != '64bit':
            raise Exception('architecture mismatch: Visual C++ environment '
                            'is configured for 64-bit but Python is 32-bit')
    else:
        if py_arch != '32bit':
            raise Exception('architecture mismatch: Visual C++ environment '
                            'is configured for 32-bit but Python is 64-bit')

    if py_version != 2:
        raise Exception('Only Python 2 is currently supported')

    build_dir.mkdir(exist_ok=True)

    gettext_pkg = download_entry(DOWNLOADS['gettext'], build_dir)
    gettext_dep_pkg = download_entry(DOWNLOADS['gettext-dep'], build_dir)
    virtualenv_pkg = download_entry(DOWNLOADS['virtualenv'], build_dir)
    py2exe_pkg = download_entry(DOWNLOADS['py2exe'], build_dir)

    venv_path = build_dir / ('venv-inno-%s' % ('x64' if vc_x64 else 'x86'))

    gettext_root = build_dir / (
        'gettext-win-%s' % DOWNLOADS['gettext']['version'])

    if not gettext_root.exists():
        extract_zip_to_directory(gettext_pkg, gettext_root)
        extract_zip_to_directory(gettext_dep_pkg, gettext_root)

    with tempfile.TemporaryDirectory() as td:
        td = pathlib.Path(td)

        # This assumes Python 2.
        extract_tar_to_directory(virtualenv_pkg, td)
        extract_zip_to_directory(py2exe_pkg, td)

        virtualenv_src_path = td / ('virtualenv-%s' %
            DOWNLOADS['virtualenv']['version'])
        py2exe_source_path = td / ('py2exe-%s' %
            DOWNLOADS['py2exe']['version'])

        virtualenv_py = virtualenv_src_path / 'virtualenv.py'

        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'

        requirements_txt = (source_dir / 'contrib' / 'packaging' /
                            'inno' / 'requirements.txt')
        subprocess.run([str(venv_pip), 'install', '-r', str(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'

        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', '-b', '3' if vc_x64 else '2',
             'build_doc', '--html'],
            cwd=str(source_dir),
            env=env,
            check=True)

        # hg.exe depends on VC9 runtime DLLs. Copy those into place.
        for f in find_vc_runtime_files(vc_x64):
            if f.name.endswith('.manifest'):
                basename = 'Microsoft.VC90.CRT.manifest'
            else:
                basename = f.name

            dest_path = source_dir / 'dist' / basename

            print('copying %s to %s' % (f, dest_path))
            shutil.copyfile(f, dest_path)

        print('creating installer')

        args = [str(iscc_exe)]

        if vc_x64:
            args.append('/dARCH=x64')

        if version:
            args.append('/dVERSION=%s' % version)

        args.append('/Odist')
        args.append('contrib/packaging/inno/mercurial.iss')

        subprocess.run(args, cwd=str(source_dir), check=True)


if __name__ == '__main__':
    parser = argparse.ArgumentParser()

    parser.add_argument('--python',
                        required=True,
                        help='path to python.exe to use')
    parser.add_argument('--iscc',
                        help='path to iscc.exe to use')
    parser.add_argument('--version',
                        help='Mercurial version string to use '
                             '(detected from __version__.py if not defined')

    args = parser.parse_args()

    if args.iscc:
        iscc = pathlib.Path(args.iscc)
    else:
        iscc = (pathlib.Path(os.environ['ProgramFiles(x86)']) / 'Inno Setup 5' /
            'ISCC.exe')

    here = pathlib.Path(os.path.abspath(os.path.dirname(__file__)))
    source_dir = here.parent.parent.parent
    build_dir = source_dir / 'build'

    sys.path.insert(0, str(source_dir / 'contrib' / 'packaging'))

    build(source_dir, build_dir, pathlib.Path(args.python), iscc,
          version=args.version)