contrib/packaging/hgpackaging/wix.py
changeset 43076 2372284d9457
parent 42049 1711f5813a63
child 43522 ce96be208ea4
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
    13 import subprocess
    13 import subprocess
    14 import tempfile
    14 import tempfile
    15 import typing
    15 import typing
    16 import xml.dom.minidom
    16 import xml.dom.minidom
    17 
    17 
    18 from .downloads import (
    18 from .downloads import download_entry
    19     download_entry,
    19 from .py2exe import build_py2exe
    20 )
       
    21 from .py2exe import (
       
    22     build_py2exe,
       
    23 )
       
    24 from .util import (
    20 from .util import (
    25     extract_zip_to_directory,
    21     extract_zip_to_directory,
    26     sign_with_signtool,
    22     sign_with_signtool,
    27 )
    23 )
    28 
    24 
    82     return '.'.join('%d' % x for x in (major, minor, build))
    78     return '.'.join('%d' % x for x in (major, minor, build))
    83 
    79 
    84 
    80 
    85 def ensure_vc90_merge_modules(build_dir):
    81 def ensure_vc90_merge_modules(build_dir):
    86     x86 = (
    82     x86 = (
    87         download_entry('vc9-crt-x86-msm', build_dir,
    83         download_entry(
    88                        local_name='microsoft.vcxx.crt.x86_msm.msm')[0],
    84             'vc9-crt-x86-msm',
    89         download_entry('vc9-crt-x86-msm-policy', build_dir,
    85             build_dir,
    90                        local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm')[0]
    86             local_name='microsoft.vcxx.crt.x86_msm.msm',
       
    87         )[0],
       
    88         download_entry(
       
    89             'vc9-crt-x86-msm-policy',
       
    90             build_dir,
       
    91             local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm',
       
    92         )[0],
    91     )
    93     )
    92 
    94 
    93     x64 = (
    95     x64 = (
    94         download_entry('vc9-crt-x64-msm', build_dir,
    96         download_entry(
    95                        local_name='microsoft.vcxx.crt.x64_msm.msm')[0],
    97             'vc9-crt-x64-msm',
    96         download_entry('vc9-crt-x64-msm-policy', build_dir,
    98             build_dir,
    97                        local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm')[0]
    99             local_name='microsoft.vcxx.crt.x64_msm.msm',
       
   100         )[0],
       
   101         download_entry(
       
   102             'vc9-crt-x64-msm-policy',
       
   103             build_dir,
       
   104             local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm',
       
   105         )[0],
    98     )
   106     )
    99     return {
   107     return {
   100         'x86': x86,
   108         'x86': x86,
   101         'x64': x64,
   109         'x64': x64,
   102     }
   110     }
   114         args.extend('-d%s=%s' % define for define in sorted(defines.items()))
   122         args.extend('-d%s=%s' % define for define in sorted(defines.items()))
   115 
   123 
   116     subprocess.run(args, cwd=str(cwd), check=True)
   124     subprocess.run(args, cwd=str(cwd), check=True)
   117 
   125 
   118 
   126 
   119 def make_post_build_signing_fn(name, subject_name=None, cert_path=None,
   127 def make_post_build_signing_fn(
   120                                cert_password=None, timestamp_url=None):
   128     name,
       
   129     subject_name=None,
       
   130     cert_path=None,
       
   131     cert_password=None,
       
   132     timestamp_url=None,
       
   133 ):
   121     """Create a callable that will use signtool to sign hg.exe."""
   134     """Create a callable that will use signtool to sign hg.exe."""
   122 
   135 
   123     def post_build_sign(source_dir, build_dir, dist_dir, version):
   136     def post_build_sign(source_dir, build_dir, dist_dir, version):
   124         description = '%s %s' % (name, version)
   137         description = '%s %s' % (name, version)
   125 
   138 
   126         sign_with_signtool(dist_dir / 'hg.exe', description,
   139         sign_with_signtool(
   127                            subject_name=subject_name, cert_path=cert_path,
   140             dist_dir / 'hg.exe',
   128                            cert_password=cert_password,
   141             description,
   129                            timestamp_url=timestamp_url)
   142             subject_name=subject_name,
       
   143             cert_path=cert_path,
       
   144             cert_password=cert_password,
       
   145             timestamp_url=timestamp_url,
       
   146         )
   130 
   147 
   131     return post_build_sign
   148     return post_build_sign
   132 
   149 
   133 
   150 
   134 LIBRARIES_XML = '''
   151 LIBRARIES_XML = '''
   153 def make_libraries_xml(wix_dir: pathlib.Path, dist_dir: pathlib.Path):
   170 def make_libraries_xml(wix_dir: pathlib.Path, dist_dir: pathlib.Path):
   154     """Make XML data for library components WXS."""
   171     """Make XML data for library components WXS."""
   155     # We can't use ElementTree because it doesn't handle the
   172     # We can't use ElementTree because it doesn't handle the
   156     # <?include ?> directives.
   173     # <?include ?> directives.
   157     doc = xml.dom.minidom.parseString(
   174     doc = xml.dom.minidom.parseString(
   158         LIBRARIES_XML.format(wix_dir=str(wix_dir)))
   175         LIBRARIES_XML.format(wix_dir=str(wix_dir))
       
   176     )
   159 
   177 
   160     component = doc.getElementsByTagName('Component')[0]
   178     component = doc.getElementsByTagName('Component')[0]
   161 
   179 
   162     f = doc.createElement('File')
   180     f = doc.createElement('File')
   163     f.setAttribute('Name', 'library.zip')
   181     f.setAttribute('Name', 'library.zip')
   175         component.appendChild(f)
   193         component.appendChild(f)
   176 
   194 
   177     return doc.toprettyxml()
   195     return doc.toprettyxml()
   178 
   196 
   179 
   197 
   180 def build_installer(source_dir: pathlib.Path, python_exe: pathlib.Path,
   198 def build_installer(
   181                     msi_name='mercurial', version=None, post_build_fn=None,
   199     source_dir: pathlib.Path,
   182                     extra_packages_script=None,
   200     python_exe: pathlib.Path,
   183                     extra_wxs:typing.Optional[typing.Dict[str,str]]=None,
   201     msi_name='mercurial',
   184                     extra_features:typing.Optional[typing.List[str]]=None):
   202     version=None,
       
   203     post_build_fn=None,
       
   204     extra_packages_script=None,
       
   205     extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
       
   206     extra_features: typing.Optional[typing.List[str]] = None,
       
   207 ):
   185     """Build a WiX MSI installer.
   208     """Build a WiX MSI installer.
   186 
   209 
   187     ``source_dir`` is the path to the Mercurial source tree to use.
   210     ``source_dir`` is the path to the Mercurial source tree to use.
   188     ``arch`` is the target architecture. either ``x86`` or ``x64``.
   211     ``arch`` is the target architecture. either ``x86`` or ``x64``.
   189     ``python_exe`` is the path to the Python executable to use/bundle.
   212     ``python_exe`` is the path to the Python executable to use/bundle.
   207     dist_dir = source_dir / 'dist'
   230     dist_dir = source_dir / 'dist'
   208     wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
   231     wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
   209 
   232 
   210     requirements_txt = wix_dir / 'requirements.txt'
   233     requirements_txt = wix_dir / 'requirements.txt'
   211 
   234 
   212     build_py2exe(source_dir, hg_build_dir,
   235     build_py2exe(
   213                  python_exe, 'wix', requirements_txt,
   236         source_dir,
   214                  extra_packages=EXTRA_PACKAGES,
   237         hg_build_dir,
   215                  extra_packages_script=extra_packages_script)
   238         python_exe,
       
   239         'wix',
       
   240         requirements_txt,
       
   241         extra_packages=EXTRA_PACKAGES,
       
   242         extra_packages_script=extra_packages_script,
       
   243     )
   216 
   244 
   217     version = version or normalize_version(find_version(source_dir))
   245     version = version or normalize_version(find_version(source_dir))
   218     print('using version string: %s' % version)
   246     print('using version string: %s' % version)
   219 
   247 
   220     if post_build_fn:
   248     if post_build_fn:
   263         assert all(';' not in f for f in extra_features)
   291         assert all(';' not in f for f in extra_features)
   264         defines['MercurialExtraFeatures'] = ';'.join(extra_features)
   292         defines['MercurialExtraFeatures'] = ';'.join(extra_features)
   265 
   293 
   266     run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
   294     run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
   267 
   295 
   268     msi_path = source_dir / 'dist' / (
   296     msi_path = (
   269         '%s-%s-%s.msi' % (msi_name, version, arch))
   297         source_dir / 'dist' / ('%s-%s-%s.msi' % (msi_name, version, arch))
       
   298     )
   270 
   299 
   271     args = [
   300     args = [
   272         str(wix_path / 'light.exe'),
   301         str(wix_path / 'light.exe'),
   273         '-nologo',
   302         '-nologo',
   274         '-ext', 'WixUIExtension',
   303         '-ext',
       
   304         'WixUIExtension',
   275         '-sw1076',
   305         '-sw1076',
   276         '-spdb',
   306         '-spdb',
   277         '-o', str(msi_path),
   307         '-o',
       
   308         str(msi_path),
   278     ]
   309     ]
   279 
   310 
   280     for source, rel_path in SUPPORT_WXS:
   311     for source, rel_path in SUPPORT_WXS:
   281         assert source.endswith('.wxs')
   312         assert source.endswith('.wxs')
   282         args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
   313         args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
   284     for source, rel_path in sorted((extra_wxs or {}).items()):
   315     for source, rel_path in sorted((extra_wxs or {}).items()):
   285         assert source.endswith('.wxs')
   316         assert source.endswith('.wxs')
   286         source = os.path.basename(source)
   317         source = os.path.basename(source)
   287         args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
   318         args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
   288 
   319 
   289     args.extend([
   320     args.extend(
   290         str(build_dir / 'library.wixobj'),
   321         [
   291         str(build_dir / 'mercurial.wixobj'),
   322             str(build_dir / 'library.wixobj'),
   292     ])
   323             str(build_dir / 'mercurial.wixobj'),
       
   324         ]
       
   325     )
   293 
   326 
   294     subprocess.run(args, cwd=str(source_dir), check=True)
   327     subprocess.run(args, cwd=str(source_dir), check=True)
   295 
   328 
   296     print('%s created' % msi_path)
   329     print('%s created' % msi_path)
   297 
   330 
   298     return {
   331     return {
   299         'msi_path': msi_path,
   332         'msi_path': msi_path,
   300     }
   333     }
   301 
   334 
   302 
   335 
   303 def build_signed_installer(source_dir: pathlib.Path, python_exe: pathlib.Path,
   336 def build_signed_installer(
   304                            name: str, version=None, subject_name=None,
   337     source_dir: pathlib.Path,
   305                            cert_path=None, cert_password=None,
   338     python_exe: pathlib.Path,
   306                            timestamp_url=None, extra_packages_script=None,
   339     name: str,
   307                            extra_wxs=None, extra_features=None):
   340     version=None,
       
   341     subject_name=None,
       
   342     cert_path=None,
       
   343     cert_password=None,
       
   344     timestamp_url=None,
       
   345     extra_packages_script=None,
       
   346     extra_wxs=None,
       
   347     extra_features=None,
       
   348 ):
   308     """Build an installer with signed executables."""
   349     """Build an installer with signed executables."""
   309 
   350 
   310     post_build_fn = make_post_build_signing_fn(
   351     post_build_fn = make_post_build_signing_fn(
   311         name,
   352         name,
   312         subject_name=subject_name,
   353         subject_name=subject_name,
   313         cert_path=cert_path,
   354         cert_path=cert_path,
   314         cert_password=cert_password,
   355         cert_password=cert_password,
   315         timestamp_url=timestamp_url)
   356         timestamp_url=timestamp_url,
   316 
   357     )
   317     info = build_installer(source_dir, python_exe=python_exe,
   358 
   318                            msi_name=name.lower(), version=version,
   359     info = build_installer(
   319                            post_build_fn=post_build_fn,
   360         source_dir,
   320                            extra_packages_script=extra_packages_script,
   361         python_exe=python_exe,
   321                            extra_wxs=extra_wxs, extra_features=extra_features)
   362         msi_name=name.lower(),
       
   363         version=version,
       
   364         post_build_fn=post_build_fn,
       
   365         extra_packages_script=extra_packages_script,
       
   366         extra_wxs=extra_wxs,
       
   367         extra_features=extra_features,
       
   368     )
   322 
   369 
   323     description = '%s %s' % (name, version)
   370     description = '%s %s' % (name, version)
   324 
   371 
   325     sign_with_signtool(info['msi_path'], description,
   372     sign_with_signtool(
   326                        subject_name=subject_name, cert_path=cert_path,
   373         info['msi_path'],
   327                        cert_password=cert_password, timestamp_url=timestamp_url)
   374         description,
       
   375         subject_name=subject_name,
       
   376         cert_path=cert_path,
       
   377         cert_password=cert_password,
       
   378         timestamp_url=timestamp_url,
       
   379     )