##// END OF EJS Templates
packaging: remove py2exe / Python 2.7 support...
Gregory Szorc -
r49703:17d5e25b default
parent child Browse files
Show More
@@ -20,13 +20,7 b' HERE = pathlib.Path(os.path.abspath(os.p'
20 SOURCE_DIR = HERE.parent.parent.parent
20 SOURCE_DIR = HERE.parent.parent.parent
21
21
22
22
23 def build_inno(pyoxidizer_target=None, python=None, iscc=None, version=None):
23 def build_inno(pyoxidizer_target, iscc=None, version=None):
24 if not pyoxidizer_target and not python:
25 raise Exception("--python required unless building with PyOxidizer")
26
27 if python and not os.path.isabs(python):
28 raise Exception("--python arg must be an absolute path")
29
30 if iscc:
24 if iscc:
31 iscc = pathlib.Path(iscc)
25 iscc = pathlib.Path(iscc)
32 else:
26 else:
@@ -38,59 +32,30 b' def build_inno(pyoxidizer_target=None, p'
38
32
39 build_dir = SOURCE_DIR / "build"
33 build_dir = SOURCE_DIR / "build"
40
34
41 if pyoxidizer_target:
35 inno.build_with_pyoxidizer(
42 inno.build_with_pyoxidizer(
36 SOURCE_DIR, build_dir, pyoxidizer_target, iscc, version=version
43 SOURCE_DIR, build_dir, pyoxidizer_target, iscc, version=version
37 )
44 )
45 else:
46 inno.build_with_py2exe(
47 SOURCE_DIR,
48 build_dir,
49 pathlib.Path(python),
50 iscc,
51 version=version,
52 )
53
38
54
39
55 def build_wix(
40 def build_wix(
41 pyoxidizer_target,
56 name=None,
42 name=None,
57 pyoxidizer_target=None,
58 python=None,
59 version=None,
43 version=None,
60 sign_sn=None,
44 sign_sn=None,
61 sign_cert=None,
45 sign_cert=None,
62 sign_password=None,
46 sign_password=None,
63 sign_timestamp_url=None,
47 sign_timestamp_url=None,
64 extra_packages_script=None,
65 extra_wxs=None,
48 extra_wxs=None,
66 extra_features=None,
49 extra_features=None,
67 extra_pyoxidizer_vars=None,
50 extra_pyoxidizer_vars=None,
68 ):
51 ):
69 if not pyoxidizer_target and not python:
70 raise Exception("--python required unless building with PyOxidizer")
71
72 if python and not os.path.isabs(python):
73 raise Exception("--python arg must be an absolute path")
74
75 kwargs = {
52 kwargs = {
76 "source_dir": SOURCE_DIR,
53 "source_dir": SOURCE_DIR,
77 "version": version,
54 "version": version,
55 "target_triple": pyoxidizer_target,
56 "extra_pyoxidizer_vars": extra_pyoxidizer_vars,
78 }
57 }
79
58
80 if pyoxidizer_target:
81 fn = wix.build_installer_pyoxidizer
82 kwargs["target_triple"] = pyoxidizer_target
83 kwargs["extra_pyoxidizer_vars"] = extra_pyoxidizer_vars
84 else:
85 fn = wix.build_installer_py2exe
86 kwargs["python_exe"] = pathlib.Path(python)
87
88 if extra_packages_script:
89 if pyoxidizer_target:
90 raise Exception(
91 "pyoxidizer does not support --extra-packages-script"
92 )
93 kwargs["extra_packages_script"] = extra_packages_script
94 if extra_wxs:
59 if extra_wxs:
95 kwargs["extra_wxs"] = dict(
60 kwargs["extra_wxs"] = dict(
96 thing.split("=") for thing in extra_wxs.split(",")
61 thing.split("=") for thing in extra_wxs.split(",")
@@ -107,7 +72,7 b' def build_wix('
107 "timestamp_url": sign_timestamp_url,
72 "timestamp_url": sign_timestamp_url,
108 }
73 }
109
74
110 fn(**kwargs)
75 wix.build_installer_pyoxidizer(**kwargs)
111
76
112
77
113 def get_parser():
78 def get_parser():
@@ -119,9 +84,9 b' def get_parser():'
119 sp.add_argument(
84 sp.add_argument(
120 "--pyoxidizer-target",
85 "--pyoxidizer-target",
121 choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
86 choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
87 required=True,
122 help="Build with PyOxidizer targeting this host triple",
88 help="Build with PyOxidizer targeting this host triple",
123 )
89 )
124 sp.add_argument("--python", help="path to python.exe to use")
125 sp.add_argument("--iscc", help="path to iscc.exe to use")
90 sp.add_argument("--iscc", help="path to iscc.exe to use")
126 sp.add_argument(
91 sp.add_argument(
127 "--version",
92 "--version",
@@ -137,9 +102,9 b' def get_parser():'
137 sp.add_argument(
102 sp.add_argument(
138 "--pyoxidizer-target",
103 "--pyoxidizer-target",
139 choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
104 choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
105 required=True,
140 help="Build with PyOxidizer targeting this host triple",
106 help="Build with PyOxidizer targeting this host triple",
141 )
107 )
142 sp.add_argument("--python", help="Path to Python executable to use")
143 sp.add_argument(
108 sp.add_argument(
144 "--sign-sn",
109 "--sign-sn",
145 help="Subject name (or fragment thereof) of certificate "
110 help="Subject name (or fragment thereof) of certificate "
@@ -155,12 +120,6 b' def get_parser():'
155 )
120 )
156 sp.add_argument("--version", help="Version string to use")
121 sp.add_argument("--version", help="Version string to use")
157 sp.add_argument(
122 sp.add_argument(
158 "--extra-packages-script",
159 help=(
160 "Script to execute to include extra packages in " "py2exe binary."
161 ),
162 )
163 sp.add_argument(
164 "--extra-wxs", help="CSV of path_to_wxs_file=working_dir_for_wxs_file"
123 "--extra-wxs", help="CSV of path_to_wxs_file=working_dir_for_wxs_file"
165 )
124 )
166 sp.add_argument(
125 sp.add_argument(
@@ -25,48 +25,6 b' DOWNLOADS = {'
25 'size': 715086,
25 'size': 715086,
26 'sha256': '411f94974492fd2ecf52590cb05b1023530aec67e64154a88b1e4ebcd9c28588',
26 'sha256': '411f94974492fd2ecf52590cb05b1023530aec67e64154a88b1e4ebcd9c28588',
27 },
27 },
28 'py2exe': {
29 'url': 'https://versaweb.dl.sourceforge.net/project/py2exe/py2exe/0.6.9/py2exe-0.6.9.zip',
30 'size': 149687,
31 'sha256': '6bd383312e7d33eef2e43a5f236f9445e4f3e0f6b16333c6f183ed445c44ddbd',
32 'version': '0.6.9',
33 },
34 # The VC9 CRT merge modules aren't readily available on most systems because
35 # they are only installed as part of a full Visual Studio 2008 install.
36 # While we could potentially extract them from a Visual Studio 2008
37 # installer, it is easier to just fetch them from a known URL.
38 'vc9-crt-x86-msm': {
39 'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/Microsoft_VC90_CRT_x86.msm',
40 'size': 615424,
41 'sha256': '837e887ef31b332feb58156f429389de345cb94504228bb9a523c25a9dd3d75e',
42 },
43 'vc9-crt-x86-msm-policy': {
44 'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/policy_9_0_Microsoft_VC90_CRT_x86.msm',
45 'size': 71168,
46 'sha256': '3fbcf92e3801a0757f36c5e8d304e134a68d5cafd197a6df7734ae3e8825c940',
47 },
48 'vc9-crt-x64-msm': {
49 'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/Microsoft_VC90_CRT_x86_x64.msm',
50 'size': 662528,
51 'sha256': '50d9639b5ad4844a2285269c7551bf5157ec636e32396ddcc6f7ec5bce487a7c',
52 },
53 'vc9-crt-x64-msm-policy': {
54 'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/policy_9_0_Microsoft_VC90_CRT_x86_x64.msm',
55 'size': 71168,
56 'sha256': '0550ea1929b21239134ad3a678c944ba0f05f11087117b6cf0833e7110686486',
57 },
58 'virtualenv': {
59 'url': 'https://files.pythonhosted.org/packages/37/db/89d6b043b22052109da35416abc3c397655e4bd3cff031446ba02b9654fa/virtualenv-16.4.3.tar.gz',
60 'size': 3713208,
61 'sha256': '984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39',
62 'version': '16.4.3',
63 },
64 'wix': {
65 'url': 'https://github.com/wixtoolset/wix3/releases/download/wix3111rtm/wix311-binaries.zip',
66 'size': 34358269,
67 'sha256': '37f0a533b0978a454efb5dc3bd3598becf9660aaf4287e55bf68ca6b527d051d',
68 'version': '3.11.1',
69 },
70 }
28 }
71
29
72
30
@@ -14,29 +14,13 b' import subprocess'
14
14
15 import jinja2
15 import jinja2
16
16
17 from .py2exe import (
18 build_py2exe,
19 stage_install,
20 )
21 from .pyoxidizer import create_pyoxidizer_install_layout
17 from .pyoxidizer import create_pyoxidizer_install_layout
22 from .util import (
18 from .util import (
23 find_legacy_vc_runtime_files,
24 normalize_windows_version,
19 normalize_windows_version,
25 process_install_rules,
20 process_install_rules,
26 read_version_py,
21 read_version_py,
27 )
22 )
28
23
29 EXTRA_PACKAGES = {
30 'dulwich',
31 'keyring',
32 'pygments',
33 'win32ctypes',
34 }
35
36 EXTRA_INCLUDES = {
37 '_curses',
38 '_curses_panel',
39 }
40
24
41 EXTRA_INSTALL_RULES = [
25 EXTRA_INSTALL_RULES = [
42 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
26 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
@@ -47,80 +31,6 b' PACKAGE_FILES_METADATA = {'
47 }
31 }
48
32
49
33
50 def build_with_py2exe(
51 source_dir: pathlib.Path,
52 build_dir: pathlib.Path,
53 python_exe: pathlib.Path,
54 iscc_exe: pathlib.Path,
55 version=None,
56 ):
57 """Build the Inno installer using py2exe.
58
59 Build files will be placed in ``build_dir``.
60
61 py2exe's setup.py doesn't use setuptools. It doesn't have modern logic
62 for finding the Python 2.7 toolchain. So, we require the environment
63 to already be configured with an active toolchain.
64 """
65 if not iscc_exe.exists():
66 raise Exception('%s does not exist' % iscc_exe)
67
68 vc_x64 = r'\x64' in os.environ.get('LIB', '')
69 arch = 'x64' if vc_x64 else 'x86'
70 inno_build_dir = build_dir / ('inno-py2exe-%s' % arch)
71 staging_dir = inno_build_dir / 'stage'
72
73 requirements_txt = (
74 source_dir / 'contrib' / 'packaging' / 'requirements-windows-py2.txt'
75 )
76
77 inno_build_dir.mkdir(parents=True, exist_ok=True)
78
79 build_py2exe(
80 source_dir,
81 build_dir,
82 python_exe,
83 'inno',
84 requirements_txt,
85 extra_packages=EXTRA_PACKAGES,
86 extra_includes=EXTRA_INCLUDES,
87 )
88
89 # Purge the staging directory for every build so packaging is
90 # pristine.
91 if staging_dir.exists():
92 print('purging %s' % staging_dir)
93 shutil.rmtree(staging_dir)
94
95 # Now assemble all the packaged files into the staging directory.
96 stage_install(source_dir, staging_dir)
97
98 # We also install some extra files.
99 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
100
101 # hg.exe depends on VC9 runtime DLLs. Copy those into place.
102 for f in find_legacy_vc_runtime_files(vc_x64):
103 if f.name.endswith('.manifest'):
104 basename = 'Microsoft.VC90.CRT.manifest'
105 else:
106 basename = f.name
107
108 dest_path = staging_dir / basename
109
110 print('copying %s to %s' % (f, dest_path))
111 shutil.copyfile(f, dest_path)
112
113 build_installer(
114 source_dir,
115 inno_build_dir,
116 staging_dir,
117 iscc_exe,
118 version,
119 arch="x64" if vc_x64 else None,
120 suffix="-python2",
121 )
122
123
124 def build_with_pyoxidizer(
34 def build_with_pyoxidizer(
125 source_dir: pathlib.Path,
35 source_dir: pathlib.Path,
126 build_dir: pathlib.Path,
36 build_dir: pathlib.Path,
@@ -7,23 +7,15 b''
7
7
8 # no-check-code because Python 3 native.
8 # no-check-code because Python 3 native.
9
9
10 import distutils.version
11 import getpass
12 import glob
10 import glob
13 import os
11 import os
14 import pathlib
12 import pathlib
15 import re
13 import re
16 import shutil
14 import shutil
17 import subprocess
15 import subprocess
18 import tarfile
19 import zipfile
16 import zipfile
20
17
21
18
22 def extract_tar_to_directory(source: pathlib.Path, dest: pathlib.Path):
23 with tarfile.open(source, 'r') as tf:
24 tf.extractall(dest)
25
26
27 def extract_zip_to_directory(source: pathlib.Path, dest: pathlib.Path):
19 def extract_zip_to_directory(source: pathlib.Path, dest: pathlib.Path):
28 with zipfile.ZipFile(source, 'r') as zf:
20 with zipfile.ZipFile(source, 'r') as zf:
29 zf.extractall(dest)
21 zf.extractall(dest)
@@ -81,59 +73,6 b' def find_vc_runtime_dll(x64=False):'
81 raise Exception("could not find vcruntime140.dll")
73 raise Exception("could not find vcruntime140.dll")
82
74
83
75
84 def find_legacy_vc_runtime_files(x64=False):
85 """Finds Visual C++ Runtime DLLs to include in distribution."""
86 winsxs = pathlib.Path(os.environ['SYSTEMROOT']) / 'WinSxS'
87
88 prefix = 'amd64' if x64 else 'x86'
89
90 candidates = sorted(
91 p
92 for p in os.listdir(winsxs)
93 if p.lower().startswith('%s_microsoft.vc90.crt_' % prefix)
94 )
95
96 for p in candidates:
97 print('found candidate VC runtime: %s' % p)
98
99 # Take the newest version.
100 version = candidates[-1]
101
102 d = winsxs / version
103
104 return [
105 d / 'msvcm90.dll',
106 d / 'msvcp90.dll',
107 d / 'msvcr90.dll',
108 winsxs / 'Manifests' / ('%s.manifest' % version),
109 ]
110
111
112 def windows_10_sdk_info():
113 """Resolves information about the Windows 10 SDK."""
114
115 base = pathlib.Path(os.environ['ProgramFiles(x86)']) / 'Windows Kits' / '10'
116
117 if not base.is_dir():
118 raise Exception('unable to find Windows 10 SDK at %s' % base)
119
120 # Find the latest version.
121 bin_base = base / 'bin'
122
123 versions = [v for v in os.listdir(bin_base) if v.startswith('10.')]
124 version = sorted(versions, reverse=True)[0]
125
126 bin_version = bin_base / version
127
128 return {
129 'root': base,
130 'version': version,
131 'bin_root': bin_version,
132 'bin_x86': bin_version / 'x86',
133 'bin_x64': bin_version / 'x64',
134 }
135
136
137 def normalize_windows_version(version):
76 def normalize_windows_version(version):
138 """Normalize Mercurial version string so WiX/Inno accepts it.
77 """Normalize Mercurial version string so WiX/Inno accepts it.
139
78
@@ -194,93 +133,6 b' def normalize_windows_version(version):'
194 return '.'.join('%d' % x for x in versions[0:4])
133 return '.'.join('%d' % x for x in versions[0:4])
195
134
196
135
197 def find_signtool():
198 """Find signtool.exe from the Windows SDK."""
199 sdk = windows_10_sdk_info()
200
201 for key in ('bin_x64', 'bin_x86'):
202 p = sdk[key] / 'signtool.exe'
203
204 if p.exists():
205 return p
206
207 raise Exception('could not find signtool.exe in Windows 10 SDK')
208
209
210 def sign_with_signtool(
211 file_path,
212 description,
213 subject_name=None,
214 cert_path=None,
215 cert_password=None,
216 timestamp_url=None,
217 ):
218 """Digitally sign a file with signtool.exe.
219
220 ``file_path`` is file to sign.
221 ``description`` is text that goes in the signature.
222
223 The signing certificate can be specified by ``cert_path`` or
224 ``subject_name``. These correspond to the ``/f`` and ``/n`` arguments
225 to signtool.exe, respectively.
226
227 The certificate password can be specified via ``cert_password``. If
228 not provided, you will be prompted for the password.
229
230 ``timestamp_url`` is the URL of a RFC 3161 timestamp server (``/tr``
231 argument to signtool.exe).
232 """
233 if cert_path and subject_name:
234 raise ValueError('cannot specify both cert_path and subject_name')
235
236 while cert_path and not cert_password:
237 cert_password = getpass.getpass('password for %s: ' % cert_path)
238
239 args = [
240 str(find_signtool()),
241 'sign',
242 '/v',
243 '/fd',
244 'sha256',
245 '/d',
246 description,
247 ]
248
249 if cert_path:
250 args.extend(['/f', str(cert_path), '/p', cert_password])
251 elif subject_name:
252 args.extend(['/n', subject_name])
253
254 if timestamp_url:
255 args.extend(['/tr', timestamp_url, '/td', 'sha256'])
256
257 args.append(str(file_path))
258
259 print('signing %s' % file_path)
260 subprocess.run(args, check=True)
261
262
263 PRINT_PYTHON_INFO = '''
264 import platform; print("%s:%s" % (platform.architecture()[0], platform.python_version()))
265 '''.strip()
266
267
268 def python_exe_info(python_exe: pathlib.Path):
269 """Obtain information about a Python executable."""
270
271 res = subprocess.check_output([str(python_exe), '-c', PRINT_PYTHON_INFO])
272
273 arch, version = res.decode('utf-8').split(':')
274
275 version = distutils.version.LooseVersion(version)
276
277 return {
278 'arch': arch,
279 'version': version,
280 'py3': version >= distutils.version.LooseVersion('3'),
281 }
282
283
284 def process_install_rules(
136 def process_install_rules(
285 rules: list, source_dir: pathlib.Path, dest_dir: pathlib.Path
137 rules: list, source_dir: pathlib.Path, dest_dir: pathlib.Path
286 ):
138 ):
@@ -7,376 +7,16 b''
7
7
8 # no-check-code because Python 3 native.
8 # no-check-code because Python 3 native.
9
9
10 import collections
11 import json
10 import json
12 import os
11 import os
13 import pathlib
12 import pathlib
14 import re
15 import shutil
13 import shutil
16 import subprocess
17 import typing
14 import typing
18 import uuid
19 import xml.dom.minidom
20
15
21 from .downloads import download_entry
22 from .py2exe import (
23 build_py2exe,
24 stage_install,
25 )
26 from .pyoxidizer import (
16 from .pyoxidizer import (
27 build_docs_html,
17 build_docs_html,
28 create_pyoxidizer_install_layout,
29 run_pyoxidizer,
18 run_pyoxidizer,
30 )
19 )
31 from .util import (
32 extract_zip_to_directory,
33 normalize_windows_version,
34 process_install_rules,
35 sign_with_signtool,
36 )
37
38
39 EXTRA_PACKAGES = {
40 'dulwich',
41 'distutils',
42 'keyring',
43 'pygments',
44 'win32ctypes',
45 }
46
47 EXTRA_INCLUDES = {
48 '_curses',
49 '_curses_panel',
50 }
51
52 EXTRA_INSTALL_RULES = [
53 ('contrib/packaging/wix/COPYING.rtf', 'COPYING.rtf'),
54 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
55 ]
56
57 STAGING_REMOVE_FILES = [
58 # We use the RTF variant.
59 'copying.txt',
60 ]
61
62 SHORTCUTS = {
63 # hg.1.html'
64 'hg.file.5d3e441c_28d9_5542_afd0_cdd4234f12d5': {
65 'Name': 'Mercurial Command Reference',
66 },
67 # hgignore.5.html
68 'hg.file.5757d8e0_f207_5e10_a2ec_3ba0a062f431': {
69 'Name': 'Mercurial Ignore Files',
70 },
71 # hgrc.5.html
72 'hg.file.92e605fd_1d1a_5dc6_9fc0_5d2998eb8f5e': {
73 'Name': 'Mercurial Configuration Files',
74 },
75 }
76
77
78 def find_version(source_dir: pathlib.Path):
79 version_py = source_dir / 'mercurial' / '__version__.py'
80
81 with version_py.open('r', encoding='utf-8') as fh:
82 source = fh.read().strip()
83
84 m = re.search('version = b"(.*)"', source)
85 return m.group(1)
86
87
88 def ensure_vc90_merge_modules(build_dir):
89 x86 = (
90 download_entry(
91 'vc9-crt-x86-msm',
92 build_dir,
93 local_name='microsoft.vcxx.crt.x86_msm.msm',
94 )[0],
95 download_entry(
96 'vc9-crt-x86-msm-policy',
97 build_dir,
98 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm',
99 )[0],
100 )
101
102 x64 = (
103 download_entry(
104 'vc9-crt-x64-msm',
105 build_dir,
106 local_name='microsoft.vcxx.crt.x64_msm.msm',
107 )[0],
108 download_entry(
109 'vc9-crt-x64-msm-policy',
110 build_dir,
111 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm',
112 )[0],
113 )
114 return {
115 'x86': x86,
116 'x64': x64,
117 }
118
119
120 def run_candle(wix, cwd, wxs, source_dir, defines=None):
121 args = [
122 str(wix / 'candle.exe'),
123 '-nologo',
124 str(wxs),
125 '-dSourceDir=%s' % source_dir,
126 ]
127
128 if defines:
129 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
130
131 subprocess.run(args, cwd=str(cwd), check=True)
132
133
134 def make_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
135 """Create XML string listing every file to be installed."""
136
137 # We derive GUIDs from a deterministic file path identifier.
138 # We shoehorn the name into something that looks like a URL because
139 # the UUID namespaces are supposed to work that way (even though
140 # the input data probably is never validated).
141
142 doc = xml.dom.minidom.parseString(
143 '<?xml version="1.0" encoding="utf-8"?>'
144 '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">'
145 '</Wix>'
146 )
147
148 # Assemble the install layout by directory. This makes it easier to
149 # emit XML, since each directory has separate entities.
150 manifest = collections.defaultdict(dict)
151
152 for root, dirs, files in os.walk(staging_dir):
153 dirs.sort()
154
155 root = pathlib.Path(root)
156 rel_dir = root.relative_to(staging_dir)
157
158 for i in range(len(rel_dir.parts)):
159 parent = '/'.join(rel_dir.parts[0 : i + 1])
160 manifest.setdefault(parent, {})
161
162 for f in sorted(files):
163 full = root / f
164 manifest[str(rel_dir).replace('\\', '/')][full.name] = full
165
166 component_groups = collections.defaultdict(list)
167
168 # Now emit a <Fragment> for each directory.
169 # Each directory is composed of a <DirectoryRef> pointing to its parent
170 # and defines child <Directory>'s and a <Component> with all the files.
171 for dir_name, entries in sorted(manifest.items()):
172 # The directory id is derived from the path. But the root directory
173 # is special.
174 if dir_name == '.':
175 parent_directory_id = 'INSTALLDIR'
176 else:
177 parent_directory_id = 'hg.dir.%s' % dir_name.replace(
178 '/', '.'
179 ).replace('-', '_')
180
181 fragment = doc.createElement('Fragment')
182 directory_ref = doc.createElement('DirectoryRef')
183 directory_ref.setAttribute('Id', parent_directory_id)
184
185 # Add <Directory> entries for immediate children directories.
186 for possible_child in sorted(manifest.keys()):
187 if (
188 dir_name == '.'
189 and '/' not in possible_child
190 and possible_child != '.'
191 ):
192 child_directory_id = ('hg.dir.%s' % possible_child).replace(
193 '-', '_'
194 )
195 name = possible_child
196 else:
197 if not possible_child.startswith('%s/' % dir_name):
198 continue
199 name = possible_child[len(dir_name) + 1 :]
200 if '/' in name:
201 continue
202
203 child_directory_id = 'hg.dir.%s' % possible_child.replace(
204 '/', '.'
205 ).replace('-', '_')
206
207 directory = doc.createElement('Directory')
208 directory.setAttribute('Id', child_directory_id)
209 directory.setAttribute('Name', name)
210 directory_ref.appendChild(directory)
211
212 # Add <Component>s for files in this directory.
213 for rel, source_path in sorted(entries.items()):
214 if dir_name == '.':
215 full_rel = rel
216 else:
217 full_rel = '%s/%s' % (dir_name, rel)
218
219 component_unique_id = (
220 'https://www.mercurial-scm.org/wix-installer/0/component/%s'
221 % full_rel
222 )
223 component_guid = uuid.uuid5(uuid.NAMESPACE_URL, component_unique_id)
224 component_id = 'hg.component.%s' % str(component_guid).replace(
225 '-', '_'
226 )
227
228 component = doc.createElement('Component')
229
230 component.setAttribute('Id', component_id)
231 component.setAttribute('Guid', str(component_guid).upper())
232 component.setAttribute('Win64', 'yes' if is_x64 else 'no')
233
234 # Assign this component to a top-level group.
235 if dir_name == '.':
236 component_groups['ROOT'].append(component_id)
237 elif '/' in dir_name:
238 component_groups[dir_name[0 : dir_name.index('/')]].append(
239 component_id
240 )
241 else:
242 component_groups[dir_name].append(component_id)
243
244 unique_id = (
245 'https://www.mercurial-scm.org/wix-installer/0/%s' % full_rel
246 )
247 file_guid = uuid.uuid5(uuid.NAMESPACE_URL, unique_id)
248
249 # IDs have length limits. So use GUID to derive them.
250 file_guid_normalized = str(file_guid).replace('-', '_')
251 file_id = 'hg.file.%s' % file_guid_normalized
252
253 file_element = doc.createElement('File')
254 file_element.setAttribute('Id', file_id)
255 file_element.setAttribute('Source', str(source_path))
256 file_element.setAttribute('KeyPath', 'yes')
257 file_element.setAttribute('ReadOnly', 'yes')
258
259 component.appendChild(file_element)
260 directory_ref.appendChild(component)
261
262 fragment.appendChild(directory_ref)
263 doc.documentElement.appendChild(fragment)
264
265 for group, component_ids in sorted(component_groups.items()):
266 fragment = doc.createElement('Fragment')
267 component_group = doc.createElement('ComponentGroup')
268 component_group.setAttribute('Id', 'hg.group.%s' % group)
269
270 for component_id in component_ids:
271 component_ref = doc.createElement('ComponentRef')
272 component_ref.setAttribute('Id', component_id)
273 component_group.appendChild(component_ref)
274
275 fragment.appendChild(component_group)
276 doc.documentElement.appendChild(fragment)
277
278 # Add <Shortcut> to files that have it defined.
279 for file_id, metadata in sorted(SHORTCUTS.items()):
280 els = doc.getElementsByTagName('File')
281 els = [el for el in els if el.getAttribute('Id') == file_id]
282
283 if not els:
284 raise Exception('could not find File[Id=%s]' % file_id)
285
286 for el in els:
287 shortcut = doc.createElement('Shortcut')
288 shortcut.setAttribute('Id', 'hg.shortcut.%s' % file_id)
289 shortcut.setAttribute('Directory', 'ProgramMenuDir')
290 shortcut.setAttribute('Icon', 'hgIcon.ico')
291 shortcut.setAttribute('IconIndex', '0')
292 shortcut.setAttribute('Advertise', 'yes')
293 for k, v in sorted(metadata.items()):
294 shortcut.setAttribute(k, v)
295
296 el.appendChild(shortcut)
297
298 return doc.toprettyxml()
299
300
301 def build_installer_py2exe(
302 source_dir: pathlib.Path,
303 python_exe: pathlib.Path,
304 msi_name='mercurial',
305 version=None,
306 extra_packages_script=None,
307 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
308 extra_features: typing.Optional[typing.List[str]] = None,
309 signing_info: typing.Optional[typing.Dict[str, str]] = None,
310 ):
311 """Build a WiX MSI installer using py2exe.
312
313 ``source_dir`` is the path to the Mercurial source tree to use.
314 ``arch`` is the target architecture. either ``x86`` or ``x64``.
315 ``python_exe`` is the path to the Python executable to use/bundle.
316 ``version`` is the Mercurial version string. If not defined,
317 ``mercurial/__version__.py`` will be consulted.
318 ``extra_packages_script`` is a command to be run to inject extra packages
319 into the py2exe binary. It should stage packages into the virtualenv and
320 print a null byte followed by a newline-separated list of packages that
321 should be included in the exe.
322 ``extra_wxs`` is a dict of {wxs_name: working_dir_for_wxs_build}.
323 ``extra_features`` is a list of additional named Features to include in
324 the build. These must match Feature names in one of the wxs scripts.
325 """
326 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
327
328 hg_build_dir = source_dir / 'build'
329
330 requirements_txt = (
331 source_dir / 'contrib' / 'packaging' / 'requirements-windows-py2.txt'
332 )
333
334 build_py2exe(
335 source_dir,
336 hg_build_dir,
337 python_exe,
338 'wix',
339 requirements_txt,
340 extra_packages=EXTRA_PACKAGES,
341 extra_packages_script=extra_packages_script,
342 extra_includes=EXTRA_INCLUDES,
343 )
344
345 build_dir = hg_build_dir / ('wix-%s' % arch)
346 staging_dir = build_dir / 'stage'
347
348 build_dir.mkdir(exist_ok=True)
349
350 # Purge the staging directory for every build so packaging is pristine.
351 if staging_dir.exists():
352 print('purging %s' % staging_dir)
353 shutil.rmtree(staging_dir)
354
355 stage_install(source_dir, staging_dir, lower_case=True)
356
357 # We also install some extra files.
358 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
359
360 # And remove some files we don't want.
361 for f in STAGING_REMOVE_FILES:
362 p = staging_dir / f
363 if p.exists():
364 print('removing %s' % p)
365 p.unlink()
366
367 return run_wix_packaging(
368 source_dir,
369 build_dir,
370 staging_dir,
371 arch,
372 version=version,
373 python2=True,
374 msi_name=msi_name,
375 suffix="-python2",
376 extra_wxs=extra_wxs,
377 extra_features=extra_features,
378 signing_info=signing_info,
379 )
380
20
381
21
382 def build_installer_pyoxidizer(
22 def build_installer_pyoxidizer(
@@ -454,133 +94,3 b' def build_installer_pyoxidizer('
454 return {
94 return {
455 "msi_path": dist_path,
95 "msi_path": dist_path,
456 }
96 }
457
458
459 def run_wix_packaging(
460 source_dir: pathlib.Path,
461 build_dir: pathlib.Path,
462 staging_dir: pathlib.Path,
463 arch: str,
464 version: str,
465 python2: bool,
466 msi_name: typing.Optional[str] = "mercurial",
467 suffix: str = "",
468 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
469 extra_features: typing.Optional[typing.List[str]] = None,
470 signing_info: typing.Optional[typing.Dict[str, str]] = None,
471 ):
472 """Invokes WiX to package up a built Mercurial.
473
474 ``signing_info`` is a dict defining properties to facilitate signing the
475 installer. Recognized keys include ``name``, ``subject_name``,
476 ``cert_path``, ``cert_password``, and ``timestamp_url``. If populated,
477 we will sign both the hg.exe and the .msi using the signing credentials
478 specified.
479 """
480
481 orig_version = version or find_version(source_dir)
482 version = normalize_windows_version(orig_version)
483 print('using version string: %s' % version)
484 if version != orig_version:
485 print('(normalized from: %s)' % orig_version)
486
487 if signing_info:
488 sign_with_signtool(
489 staging_dir / "hg.exe",
490 "%s %s" % (signing_info["name"], version),
491 subject_name=signing_info["subject_name"],
492 cert_path=signing_info["cert_path"],
493 cert_password=signing_info["cert_password"],
494 timestamp_url=signing_info["timestamp_url"],
495 )
496
497 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
498
499 wix_pkg, wix_entry = download_entry('wix', build_dir)
500 wix_path = build_dir / ('wix-%s' % wix_entry['version'])
501
502 if not wix_path.exists():
503 extract_zip_to_directory(wix_pkg, wix_path)
504
505 if python2:
506 ensure_vc90_merge_modules(build_dir)
507
508 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
509
510 defines = {'Platform': arch}
511
512 # Derive a .wxs file with the staged files.
513 manifest_wxs = build_dir / 'stage.wxs'
514 with manifest_wxs.open('w', encoding='utf-8') as fh:
515 fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
516
517 run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
518
519 for source, rel_path in sorted((extra_wxs or {}).items()):
520 run_candle(wix_path, build_dir, source, rel_path, defines=defines)
521
522 source = wix_dir / 'mercurial.wxs'
523 defines['Version'] = version
524 defines['Comments'] = 'Installs Mercurial version %s' % version
525
526 if python2:
527 defines["PythonVersion"] = "2"
528 defines['VCRedistSrcDir'] = str(build_dir)
529 else:
530 defines["PythonVersion"] = "3"
531
532 if (staging_dir / "lib").exists():
533 defines["MercurialHasLib"] = "1"
534
535 if extra_features:
536 assert all(';' not in f for f in extra_features)
537 defines['MercurialExtraFeatures'] = ';'.join(extra_features)
538
539 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
540
541 msi_path = (
542 source_dir
543 / 'dist'
544 / ('%s-%s-%s%s.msi' % (msi_name, orig_version, arch, suffix))
545 )
546
547 args = [
548 str(wix_path / 'light.exe'),
549 '-nologo',
550 '-ext',
551 'WixUIExtension',
552 '-sw1076',
553 '-spdb',
554 '-o',
555 str(msi_path),
556 ]
557
558 for source, rel_path in sorted((extra_wxs or {}).items()):
559 assert source.endswith('.wxs')
560 source = os.path.basename(source)
561 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
562
563 args.extend(
564 [
565 str(build_dir / 'stage.wixobj'),
566 str(build_dir / 'mercurial.wixobj'),
567 ]
568 )
569
570 subprocess.run(args, cwd=str(source_dir), check=True)
571
572 print('%s created' % msi_path)
573
574 if signing_info:
575 sign_with_signtool(
576 msi_path,
577 "%s %s" % (signing_info["name"], version),
578 subject_name=signing_info["subject_name"],
579 cert_path=signing_info["cert_path"],
580 cert_password=signing_info["cert_password"],
581 timestamp_url=signing_info["timestamp_url"],
582 )
583
584 return {
585 'msi_path': msi_path,
586 }
@@ -27,7 +27,6 b' New errors are not allowed. Warnings are'
27 Skipping contrib/packaging/hgpackaging/cli.py it has no-che?k-code (glob)
27 Skipping contrib/packaging/hgpackaging/cli.py it has no-che?k-code (glob)
28 Skipping contrib/packaging/hgpackaging/downloads.py it has no-che?k-code (glob)
28 Skipping contrib/packaging/hgpackaging/downloads.py it has no-che?k-code (glob)
29 Skipping contrib/packaging/hgpackaging/inno.py it has no-che?k-code (glob)
29 Skipping contrib/packaging/hgpackaging/inno.py it has no-che?k-code (glob)
30 Skipping contrib/packaging/hgpackaging/py2exe.py it has no-che?k-code (glob)
31 Skipping contrib/packaging/hgpackaging/pyoxidizer.py it has no-che?k-code (glob)
30 Skipping contrib/packaging/hgpackaging/pyoxidizer.py it has no-che?k-code (glob)
32 Skipping contrib/packaging/hgpackaging/util.py it has no-che?k-code (glob)
31 Skipping contrib/packaging/hgpackaging/util.py it has no-che?k-code (glob)
33 Skipping contrib/packaging/hgpackaging/wix.py it has no-che?k-code (glob)
32 Skipping contrib/packaging/hgpackaging/wix.py it has no-che?k-code (glob)
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now