contrib/packaging/inno/build.py
changeset 41853 d7dc4ac1ff84
child 41854 7a1433e90482
equal deleted inserted replaced
41852:db3098d02a6d 41853:d7dc4ac1ff84
       
     1 #!/usr/bin/env python3
       
     2 # build.py - Inno installer build script.
       
     3 #
       
     4 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
       
     5 #
       
     6 # This software may be used and distributed according to the terms of the
       
     7 # GNU General Public License version 2 or any later version.
       
     8 
       
     9 # This script automates the building of the Inno MSI installer for Mercurial.
       
    10 
       
    11 # no-check-code because Python 3 native.
       
    12 
       
    13 import argparse
       
    14 import os
       
    15 import pathlib
       
    16 import shutil
       
    17 import subprocess
       
    18 import sys
       
    19 import tempfile
       
    20 
       
    21 
       
    22 DOWNLOADS = {
       
    23     'gettext': {
       
    24         'url': 'https://versaweb.dl.sourceforge.net/project/gnuwin32/gettext/0.14.4/gettext-0.14.4-bin.zip',
       
    25         'size': 1606131,
       
    26         'sha256': '60b9ef26bc5cceef036f0424e542106cf158352b2677f43a01affd6d82a1d641',
       
    27         'version': '0.14.4',
       
    28     },
       
    29     'gettext-dep': {
       
    30         'url': 'https://versaweb.dl.sourceforge.net/project/gnuwin32/gettext/0.14.4/gettext-0.14.4-dep.zip',
       
    31         'size': 715086,
       
    32         'sha256': '411f94974492fd2ecf52590cb05b1023530aec67e64154a88b1e4ebcd9c28588',
       
    33     },
       
    34     'py2exe': {
       
    35         'url': 'https://versaweb.dl.sourceforge.net/project/py2exe/py2exe/0.6.9/py2exe-0.6.9.zip',
       
    36         'size': 149687,
       
    37         'sha256': '6bd383312e7d33eef2e43a5f236f9445e4f3e0f6b16333c6f183ed445c44ddbd',
       
    38         'version': '0.6.9',
       
    39     },
       
    40     'virtualenv': {
       
    41         'url': 'https://files.pythonhosted.org/packages/37/db/89d6b043b22052109da35416abc3c397655e4bd3cff031446ba02b9654fa/virtualenv-16.4.3.tar.gz',
       
    42         'size': 3713208,
       
    43         'sha256': '984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39',
       
    44         'version': '16.4.3',
       
    45     },
       
    46 }
       
    47 
       
    48 
       
    49 PRINT_PYTHON_INFO = '''
       
    50 import platform, sys; print("%s:%d" % (platform.architecture()[0], sys.version_info[0]))
       
    51 '''.strip()
       
    52 
       
    53 
       
    54 def find_vc_runtime_files(x64=False):
       
    55     """Finds Visual C++ Runtime DLLs to include in distribution."""
       
    56     winsxs = pathlib.Path(os.environ['SYSTEMROOT']) / 'WinSxS'
       
    57 
       
    58     prefix = 'amd64' if x64 else 'x86'
       
    59 
       
    60     candidates = sorted(p for p in os.listdir(winsxs)
       
    61                   if p.lower().startswith('%s_microsoft.vc90.crt_' % prefix))
       
    62 
       
    63     for p in candidates:
       
    64         print('found candidate VC runtime: %s' % p)
       
    65 
       
    66     # Take the newest version.
       
    67     version = candidates[-1]
       
    68 
       
    69     d = winsxs / version
       
    70 
       
    71     return [
       
    72         d / 'msvcm90.dll',
       
    73         d / 'msvcp90.dll',
       
    74         d / 'msvcr90.dll',
       
    75         winsxs / 'Manifests' / ('%s.manifest' % version),
       
    76     ]
       
    77 
       
    78 
       
    79 def build(source_dir: pathlib.Path, build_dir: pathlib.Path,
       
    80           python_exe: pathlib.Path, iscc_exe: pathlib.Path,
       
    81           version=None):
       
    82     """Build the Inno installer.
       
    83 
       
    84     Build files will be placed in ``build_dir``.
       
    85 
       
    86     py2exe's setup.py doesn't use setuptools. It doesn't have modern logic
       
    87     for finding the Python 2.7 toolchain. So, we require the environment
       
    88     to already be configured with an active toolchain.
       
    89     """
       
    90     from packagingutil import (
       
    91         download_entry,
       
    92         extract_tar_to_directory,
       
    93         extract_zip_to_directory,
       
    94     )
       
    95 
       
    96     if not iscc.exists():
       
    97         raise Exception('%s does not exist' % iscc)
       
    98 
       
    99     if 'VCINSTALLDIR' not in os.environ:
       
   100         raise Exception('not running from a Visual C++ build environment; '
       
   101                         'execute the "Visual C++ <version> Command Prompt" '
       
   102                         'application shortcut or a vcsvarsall.bat file')
       
   103 
       
   104     # Identity x86/x64 and validate the environment matches the Python
       
   105     # architecture.
       
   106     vc_x64 = r'\x64' in os.environ['LIB']
       
   107 
       
   108     res = subprocess.run(
       
   109         [str(python_exe), '-c', PRINT_PYTHON_INFO],
       
   110         capture_output=True, check=True)
       
   111 
       
   112     py_arch, py_version = res.stdout.decode('utf-8').split(':')
       
   113     py_version = int(py_version)
       
   114 
       
   115     if vc_x64:
       
   116         if py_arch != '64bit':
       
   117             raise Exception('architecture mismatch: Visual C++ environment '
       
   118                             'is configured for 64-bit but Python is 32-bit')
       
   119     else:
       
   120         if py_arch != '32bit':
       
   121             raise Exception('architecture mismatch: Visual C++ environment '
       
   122                             'is configured for 32-bit but Python is 64-bit')
       
   123 
       
   124     if py_version != 2:
       
   125         raise Exception('Only Python 2 is currently supported')
       
   126 
       
   127     # Some extensions may require DLLs from the Universal C Runtime (UCRT).
       
   128     # These are typically not in PATH and py2exe will have trouble finding
       
   129     # them. We find the Windows 10 SDK and the UCRT files within.
       
   130     sdk_path = (pathlib.Path(os.environ['ProgramFiles(x86)']) /
       
   131                 'Windows Kits' / '10' / 'Redist' / 'ucrt' / 'DLLs')
       
   132 
       
   133     if vc_x64:
       
   134         sdk_path = sdk_path / 'x64'
       
   135     else:
       
   136         sdk_path = sdk_path / 'x86'
       
   137 
       
   138     if not sdk_path.is_dir():
       
   139         raise Exception('UCRT files could not be found at %s' % sdk_path)
       
   140 
       
   141     build_dir.mkdir(exist_ok=True)
       
   142 
       
   143     gettext_pkg = download_entry(DOWNLOADS['gettext'], build_dir)
       
   144     gettext_dep_pkg = download_entry(DOWNLOADS['gettext-dep'], build_dir)
       
   145     virtualenv_pkg = download_entry(DOWNLOADS['virtualenv'], build_dir)
       
   146     py2exe_pkg = download_entry(DOWNLOADS['py2exe'], build_dir)
       
   147 
       
   148     venv_path = build_dir / ('venv-inno-%s' % ('x64' if vc_x64 else 'x86'))
       
   149 
       
   150     gettext_root = build_dir / (
       
   151         'gettext-win-%s' % DOWNLOADS['gettext']['version'])
       
   152 
       
   153     if not gettext_root.exists():
       
   154         extract_zip_to_directory(gettext_pkg, gettext_root)
       
   155         extract_zip_to_directory(gettext_dep_pkg, gettext_root)
       
   156 
       
   157     with tempfile.TemporaryDirectory() as td:
       
   158         td = pathlib.Path(td)
       
   159 
       
   160         # This assumes Python 2.
       
   161         extract_tar_to_directory(virtualenv_pkg, td)
       
   162         extract_zip_to_directory(py2exe_pkg, td)
       
   163 
       
   164         virtualenv_src_path = td / ('virtualenv-%s' %
       
   165             DOWNLOADS['virtualenv']['version'])
       
   166         py2exe_source_path = td / ('py2exe-%s' %
       
   167             DOWNLOADS['py2exe']['version'])
       
   168 
       
   169         virtualenv_py = virtualenv_src_path / 'virtualenv.py'
       
   170 
       
   171         if not venv_path.exists():
       
   172             print('creating virtualenv with dependencies')
       
   173             subprocess.run(
       
   174                 [str(python_exe), str(virtualenv_py), str(venv_path)],
       
   175                 check=True)
       
   176 
       
   177         venv_python = venv_path / 'Scripts' / 'python.exe'
       
   178         venv_pip = venv_path / 'Scripts' / 'pip.exe'
       
   179 
       
   180         requirements_txt = (source_dir / 'contrib' / 'packaging' /
       
   181                             'inno' / 'requirements.txt')
       
   182         subprocess.run([str(venv_pip), 'install', '-r', str(requirements_txt)],
       
   183                        check=True)
       
   184 
       
   185         # Force distutils to use VC++ settings from environment, which was
       
   186         # validated above.
       
   187         env = dict(os.environ)
       
   188         env['DISTUTILS_USE_SDK'] = '1'
       
   189         env['MSSdk'] = '1'
       
   190 
       
   191         py2exe_py_path = venv_path / 'Lib' / 'site-packages' / 'py2exe'
       
   192         if not py2exe_py_path.exists():
       
   193             print('building py2exe')
       
   194             subprocess.run([str(venv_python), 'setup.py', 'install'],
       
   195                            cwd=py2exe_source_path,
       
   196                            env=env,
       
   197                            check=True)
       
   198 
       
   199         if str(sdk_path) not in os.environ['PATH'].split(os.pathsep):
       
   200             print('adding %s to PATH' % sdk_path)
       
   201             env['PATH'] = '%s%s%s' % (
       
   202                 os.environ['PATH'], os.pathsep, str(sdk_path))
       
   203 
       
   204         # Register location of msgfmt and other binaries.
       
   205         env['PATH'] = '%s%s%s' % (
       
   206             env['PATH'], os.pathsep, str(gettext_root / 'bin'))
       
   207 
       
   208         print('building Mercurial')
       
   209         subprocess.run(
       
   210             [str(venv_python), 'setup.py',
       
   211              'py2exe', '-b', '3' if vc_x64 else '2',
       
   212              'build_doc', '--html'],
       
   213             cwd=str(source_dir),
       
   214             env=env,
       
   215             check=True)
       
   216 
       
   217         # hg.exe depends on VC9 runtime DLLs. Copy those into place.
       
   218         for f in find_vc_runtime_files(vc_x64):
       
   219             if f.name.endswith('.manifest'):
       
   220                 basename = 'Microsoft.VC90.CRT.manifest'
       
   221             else:
       
   222                 basename = f.name
       
   223 
       
   224             dest_path = source_dir / 'dist' / basename
       
   225 
       
   226             print('copying %s to %s' % (f, dest_path))
       
   227             shutil.copyfile(f, dest_path)
       
   228 
       
   229         print('creating installer')
       
   230 
       
   231         args = [str(iscc_exe)]
       
   232 
       
   233         if vc_x64:
       
   234             args.append('/dARCH=x64')
       
   235 
       
   236         if version:
       
   237             args.append('/dVERSION=%s' % version)
       
   238 
       
   239         args.append('/Odist')
       
   240         args.append('contrib/packaging/inno/mercurial.iss')
       
   241 
       
   242         subprocess.run(args, cwd=str(source_dir), check=True)
       
   243 
       
   244 
       
   245 if __name__ == '__main__':
       
   246     parser = argparse.ArgumentParser()
       
   247 
       
   248     parser.add_argument('--python',
       
   249                         required=True,
       
   250                         help='path to python.exe to use')
       
   251     parser.add_argument('--iscc',
       
   252                         help='path to iscc.exe to use')
       
   253     parser.add_argument('--version',
       
   254                         help='Mercurial version string to use '
       
   255                              '(detected from __version__.py if not defined')
       
   256 
       
   257     args = parser.parse_args()
       
   258 
       
   259     if args.iscc:
       
   260         iscc = pathlib.Path(args.iscc)
       
   261     else:
       
   262         iscc = (pathlib.Path(os.environ['ProgramFiles(x86)']) / 'Inno Setup 5' /
       
   263             'ISCC.exe')
       
   264 
       
   265     here = pathlib.Path(os.path.abspath(os.path.dirname(__file__)))
       
   266     source_dir = here.parent.parent.parent
       
   267     build_dir = source_dir / 'build'
       
   268 
       
   269     sys.path.insert(0, str(source_dir / 'contrib' / 'packaging'))
       
   270 
       
   271     build(source_dir, build_dir, pathlib.Path(args.python), iscc,
       
   272           version=args.version)