##// END OF EJS Templates
packaging: move the version normalization function to the util module...
Matt Harbison -
r44707:a70108a3 stable
parent child Browse files
Show More
@@ -1,226 +1,286 b''
1 # util.py - Common packaging utility code.
1 # util.py - Common packaging utility code.
2 #
2 #
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 # no-check-code because Python 3 native.
8 # no-check-code because Python 3 native.
9
9
10 import distutils.version
10 import distutils.version
11 import getpass
11 import getpass
12 import glob
12 import glob
13 import os
13 import os
14 import pathlib
14 import pathlib
15 import re
15 import re
16 import shutil
16 import shutil
17 import subprocess
17 import subprocess
18 import tarfile
18 import tarfile
19 import zipfile
19 import zipfile
20
20
21
21
22 def extract_tar_to_directory(source: pathlib.Path, dest: pathlib.Path):
22 def extract_tar_to_directory(source: pathlib.Path, dest: pathlib.Path):
23 with tarfile.open(source, 'r') as tf:
23 with tarfile.open(source, 'r') as tf:
24 tf.extractall(dest)
24 tf.extractall(dest)
25
25
26
26
27 def extract_zip_to_directory(source: pathlib.Path, dest: pathlib.Path):
27 def extract_zip_to_directory(source: pathlib.Path, dest: pathlib.Path):
28 with zipfile.ZipFile(source, 'r') as zf:
28 with zipfile.ZipFile(source, 'r') as zf:
29 zf.extractall(dest)
29 zf.extractall(dest)
30
30
31
31
32 def find_vc_runtime_files(x64=False):
32 def find_vc_runtime_files(x64=False):
33 """Finds Visual C++ Runtime DLLs to include in distribution."""
33 """Finds Visual C++ Runtime DLLs to include in distribution."""
34 winsxs = pathlib.Path(os.environ['SYSTEMROOT']) / 'WinSxS'
34 winsxs = pathlib.Path(os.environ['SYSTEMROOT']) / 'WinSxS'
35
35
36 prefix = 'amd64' if x64 else 'x86'
36 prefix = 'amd64' if x64 else 'x86'
37
37
38 candidates = sorted(
38 candidates = sorted(
39 p
39 p
40 for p in os.listdir(winsxs)
40 for p in os.listdir(winsxs)
41 if p.lower().startswith('%s_microsoft.vc90.crt_' % prefix)
41 if p.lower().startswith('%s_microsoft.vc90.crt_' % prefix)
42 )
42 )
43
43
44 for p in candidates:
44 for p in candidates:
45 print('found candidate VC runtime: %s' % p)
45 print('found candidate VC runtime: %s' % p)
46
46
47 # Take the newest version.
47 # Take the newest version.
48 version = candidates[-1]
48 version = candidates[-1]
49
49
50 d = winsxs / version
50 d = winsxs / version
51
51
52 return [
52 return [
53 d / 'msvcm90.dll',
53 d / 'msvcm90.dll',
54 d / 'msvcp90.dll',
54 d / 'msvcp90.dll',
55 d / 'msvcr90.dll',
55 d / 'msvcr90.dll',
56 winsxs / 'Manifests' / ('%s.manifest' % version),
56 winsxs / 'Manifests' / ('%s.manifest' % version),
57 ]
57 ]
58
58
59
59
60 def windows_10_sdk_info():
60 def windows_10_sdk_info():
61 """Resolves information about the Windows 10 SDK."""
61 """Resolves information about the Windows 10 SDK."""
62
62
63 base = pathlib.Path(os.environ['ProgramFiles(x86)']) / 'Windows Kits' / '10'
63 base = pathlib.Path(os.environ['ProgramFiles(x86)']) / 'Windows Kits' / '10'
64
64
65 if not base.is_dir():
65 if not base.is_dir():
66 raise Exception('unable to find Windows 10 SDK at %s' % base)
66 raise Exception('unable to find Windows 10 SDK at %s' % base)
67
67
68 # Find the latest version.
68 # Find the latest version.
69 bin_base = base / 'bin'
69 bin_base = base / 'bin'
70
70
71 versions = [v for v in os.listdir(bin_base) if v.startswith('10.')]
71 versions = [v for v in os.listdir(bin_base) if v.startswith('10.')]
72 version = sorted(versions, reverse=True)[0]
72 version = sorted(versions, reverse=True)[0]
73
73
74 bin_version = bin_base / version
74 bin_version = bin_base / version
75
75
76 return {
76 return {
77 'root': base,
77 'root': base,
78 'version': version,
78 'version': version,
79 'bin_root': bin_version,
79 'bin_root': bin_version,
80 'bin_x86': bin_version / 'x86',
80 'bin_x86': bin_version / 'x86',
81 'bin_x64': bin_version / 'x64',
81 'bin_x64': bin_version / 'x64',
82 }
82 }
83
83
84
84
85 def normalize_windows_version(version):
86 """Normalize Mercurial version string so WiX/Inno accepts it.
87
88 Version strings have to be numeric ``A.B.C[.D]`` to conform with MSI's
89 requirements.
90
91 We normalize RC version or the commit count to a 4th version component.
92 We store this in the 4th component because ``A.B.C`` releases do occur
93 and we want an e.g. ``5.3rc0`` version to be semantically less than a
94 ``5.3.1rc2`` version. This requires always reserving the 3rd version
95 component for the point release and the ``X.YrcN`` release is always
96 point release 0.
97
98 In the case of an RC and presence of ``+`` suffix data, we can't use both
99 because the version format is limited to 4 components. We choose to use
100 RC and throw away the commit count in the suffix. This means we could
101 produce multiple installers with the same normalized version string.
102
103 >>> normalize_windows_version("5.3")
104 '5.3.0'
105
106 >>> normalize_windows_version("5.3rc0")
107 '5.3.0.0'
108
109 >>> normalize_windows_version("5.3rc1")
110 '5.3.0.1'
111
112 >>> normalize_windows_version("5.3rc1+2-abcdef")
113 '5.3.0.1'
114
115 >>> normalize_windows_version("5.3+2-abcdef")
116 '5.3.0.2'
117 """
118 if '+' in version:
119 version, extra = version.split('+', 1)
120 else:
121 extra = None
122
123 # 4.9rc0
124 if version[:-1].endswith('rc'):
125 rc = int(version[-1:])
126 version = version[:-3]
127 else:
128 rc = None
129
130 # Ensure we have at least X.Y version components.
131 versions = [int(v) for v in version.split('.')]
132 while len(versions) < 3:
133 versions.append(0)
134
135 if len(versions) < 4:
136 if rc is not None:
137 versions.append(rc)
138 elif extra:
139 # <commit count>-<hash>+<date>
140 versions.append(int(extra.split('-')[0]))
141
142 return '.'.join('%d' % x for x in versions[0:4])
143
144
85 def find_signtool():
145 def find_signtool():
86 """Find signtool.exe from the Windows SDK."""
146 """Find signtool.exe from the Windows SDK."""
87 sdk = windows_10_sdk_info()
147 sdk = windows_10_sdk_info()
88
148
89 for key in ('bin_x64', 'bin_x86'):
149 for key in ('bin_x64', 'bin_x86'):
90 p = sdk[key] / 'signtool.exe'
150 p = sdk[key] / 'signtool.exe'
91
151
92 if p.exists():
152 if p.exists():
93 return p
153 return p
94
154
95 raise Exception('could not find signtool.exe in Windows 10 SDK')
155 raise Exception('could not find signtool.exe in Windows 10 SDK')
96
156
97
157
98 def sign_with_signtool(
158 def sign_with_signtool(
99 file_path,
159 file_path,
100 description,
160 description,
101 subject_name=None,
161 subject_name=None,
102 cert_path=None,
162 cert_path=None,
103 cert_password=None,
163 cert_password=None,
104 timestamp_url=None,
164 timestamp_url=None,
105 ):
165 ):
106 """Digitally sign a file with signtool.exe.
166 """Digitally sign a file with signtool.exe.
107
167
108 ``file_path`` is file to sign.
168 ``file_path`` is file to sign.
109 ``description`` is text that goes in the signature.
169 ``description`` is text that goes in the signature.
110
170
111 The signing certificate can be specified by ``cert_path`` or
171 The signing certificate can be specified by ``cert_path`` or
112 ``subject_name``. These correspond to the ``/f`` and ``/n`` arguments
172 ``subject_name``. These correspond to the ``/f`` and ``/n`` arguments
113 to signtool.exe, respectively.
173 to signtool.exe, respectively.
114
174
115 The certificate password can be specified via ``cert_password``. If
175 The certificate password can be specified via ``cert_password``. If
116 not provided, you will be prompted for the password.
176 not provided, you will be prompted for the password.
117
177
118 ``timestamp_url`` is the URL of a RFC 3161 timestamp server (``/tr``
178 ``timestamp_url`` is the URL of a RFC 3161 timestamp server (``/tr``
119 argument to signtool.exe).
179 argument to signtool.exe).
120 """
180 """
121 if cert_path and subject_name:
181 if cert_path and subject_name:
122 raise ValueError('cannot specify both cert_path and subject_name')
182 raise ValueError('cannot specify both cert_path and subject_name')
123
183
124 while cert_path and not cert_password:
184 while cert_path and not cert_password:
125 cert_password = getpass.getpass('password for %s: ' % cert_path)
185 cert_password = getpass.getpass('password for %s: ' % cert_path)
126
186
127 args = [
187 args = [
128 str(find_signtool()),
188 str(find_signtool()),
129 'sign',
189 'sign',
130 '/v',
190 '/v',
131 '/fd',
191 '/fd',
132 'sha256',
192 'sha256',
133 '/d',
193 '/d',
134 description,
194 description,
135 ]
195 ]
136
196
137 if cert_path:
197 if cert_path:
138 args.extend(['/f', str(cert_path), '/p', cert_password])
198 args.extend(['/f', str(cert_path), '/p', cert_password])
139 elif subject_name:
199 elif subject_name:
140 args.extend(['/n', subject_name])
200 args.extend(['/n', subject_name])
141
201
142 if timestamp_url:
202 if timestamp_url:
143 args.extend(['/tr', timestamp_url, '/td', 'sha256'])
203 args.extend(['/tr', timestamp_url, '/td', 'sha256'])
144
204
145 args.append(str(file_path))
205 args.append(str(file_path))
146
206
147 print('signing %s' % file_path)
207 print('signing %s' % file_path)
148 subprocess.run(args, check=True)
208 subprocess.run(args, check=True)
149
209
150
210
151 PRINT_PYTHON_INFO = '''
211 PRINT_PYTHON_INFO = '''
152 import platform; print("%s:%s" % (platform.architecture()[0], platform.python_version()))
212 import platform; print("%s:%s" % (platform.architecture()[0], platform.python_version()))
153 '''.strip()
213 '''.strip()
154
214
155
215
156 def python_exe_info(python_exe: pathlib.Path):
216 def python_exe_info(python_exe: pathlib.Path):
157 """Obtain information about a Python executable."""
217 """Obtain information about a Python executable."""
158
218
159 res = subprocess.check_output([str(python_exe), '-c', PRINT_PYTHON_INFO])
219 res = subprocess.check_output([str(python_exe), '-c', PRINT_PYTHON_INFO])
160
220
161 arch, version = res.decode('utf-8').split(':')
221 arch, version = res.decode('utf-8').split(':')
162
222
163 version = distutils.version.LooseVersion(version)
223 version = distutils.version.LooseVersion(version)
164
224
165 return {
225 return {
166 'arch': arch,
226 'arch': arch,
167 'version': version,
227 'version': version,
168 'py3': version >= distutils.version.LooseVersion('3'),
228 'py3': version >= distutils.version.LooseVersion('3'),
169 }
229 }
170
230
171
231
172 def process_install_rules(
232 def process_install_rules(
173 rules: list, source_dir: pathlib.Path, dest_dir: pathlib.Path
233 rules: list, source_dir: pathlib.Path, dest_dir: pathlib.Path
174 ):
234 ):
175 for source, dest in rules:
235 for source, dest in rules:
176 if '*' in source:
236 if '*' in source:
177 if not dest.endswith('/'):
237 if not dest.endswith('/'):
178 raise ValueError('destination must end in / when globbing')
238 raise ValueError('destination must end in / when globbing')
179
239
180 # We strip off the source path component before the first glob
240 # We strip off the source path component before the first glob
181 # character to construct the relative install path.
241 # character to construct the relative install path.
182 prefix_end_index = source[: source.index('*')].rindex('/')
242 prefix_end_index = source[: source.index('*')].rindex('/')
183 relative_prefix = source_dir / source[0:prefix_end_index]
243 relative_prefix = source_dir / source[0:prefix_end_index]
184
244
185 for res in glob.glob(str(source_dir / source), recursive=True):
245 for res in glob.glob(str(source_dir / source), recursive=True):
186 source_path = pathlib.Path(res)
246 source_path = pathlib.Path(res)
187
247
188 if source_path.is_dir():
248 if source_path.is_dir():
189 continue
249 continue
190
250
191 rel_path = source_path.relative_to(relative_prefix)
251 rel_path = source_path.relative_to(relative_prefix)
192
252
193 dest_path = dest_dir / dest[:-1] / rel_path
253 dest_path = dest_dir / dest[:-1] / rel_path
194
254
195 dest_path.parent.mkdir(parents=True, exist_ok=True)
255 dest_path.parent.mkdir(parents=True, exist_ok=True)
196 print('copying %s to %s' % (source_path, dest_path))
256 print('copying %s to %s' % (source_path, dest_path))
197 shutil.copy(source_path, dest_path)
257 shutil.copy(source_path, dest_path)
198
258
199 # Simple file case.
259 # Simple file case.
200 else:
260 else:
201 source_path = pathlib.Path(source)
261 source_path = pathlib.Path(source)
202
262
203 if dest.endswith('/'):
263 if dest.endswith('/'):
204 dest_path = pathlib.Path(dest) / source_path.name
264 dest_path = pathlib.Path(dest) / source_path.name
205 else:
265 else:
206 dest_path = pathlib.Path(dest)
266 dest_path = pathlib.Path(dest)
207
267
208 full_source_path = source_dir / source_path
268 full_source_path = source_dir / source_path
209 full_dest_path = dest_dir / dest_path
269 full_dest_path = dest_dir / dest_path
210
270
211 full_dest_path.parent.mkdir(parents=True, exist_ok=True)
271 full_dest_path.parent.mkdir(parents=True, exist_ok=True)
212 shutil.copy(full_source_path, full_dest_path)
272 shutil.copy(full_source_path, full_dest_path)
213 print('copying %s to %s' % (full_source_path, full_dest_path))
273 print('copying %s to %s' % (full_source_path, full_dest_path))
214
274
215
275
216 def read_version_py(source_dir):
276 def read_version_py(source_dir):
217 """Read the mercurial/__version__.py file to resolve the version string."""
277 """Read the mercurial/__version__.py file to resolve the version string."""
218 p = source_dir / 'mercurial' / '__version__.py'
278 p = source_dir / 'mercurial' / '__version__.py'
219
279
220 with p.open('r', encoding='utf-8') as fh:
280 with p.open('r', encoding='utf-8') as fh:
221 m = re.search('version = b"([^"]+)"', fh.read(), re.MULTILINE)
281 m = re.search('version = b"([^"]+)"', fh.read(), re.MULTILINE)
222
282
223 if not m:
283 if not m:
224 raise Exception('could not parse %s' % p)
284 raise Exception('could not parse %s' % p)
225
285
226 return m.group(1)
286 return m.group(1)
@@ -1,553 +1,494 b''
1 # wix.py - WiX installer functionality
1 # wix.py - WiX installer functionality
2 #
2 #
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 # no-check-code because Python 3 native.
8 # no-check-code because Python 3 native.
9
9
10 import collections
10 import collections
11 import os
11 import os
12 import pathlib
12 import pathlib
13 import re
13 import re
14 import shutil
14 import shutil
15 import subprocess
15 import subprocess
16 import typing
16 import typing
17 import uuid
17 import uuid
18 import xml.dom.minidom
18 import xml.dom.minidom
19
19
20 from .downloads import download_entry
20 from .downloads import download_entry
21 from .py2exe import (
21 from .py2exe import (
22 build_py2exe,
22 build_py2exe,
23 stage_install,
23 stage_install,
24 )
24 )
25 from .util import (
25 from .util import (
26 extract_zip_to_directory,
26 extract_zip_to_directory,
27 normalize_windows_version,
27 process_install_rules,
28 process_install_rules,
28 sign_with_signtool,
29 sign_with_signtool,
29 )
30 )
30
31
31
32
32 EXTRA_PACKAGES = {
33 EXTRA_PACKAGES = {
33 'distutils',
34 'distutils',
34 'pygments',
35 'pygments',
35 }
36 }
36
37
37
38
38 EXTRA_INSTALL_RULES = [
39 EXTRA_INSTALL_RULES = [
39 ('contrib/packaging/wix/COPYING.rtf', 'COPYING.rtf'),
40 ('contrib/packaging/wix/COPYING.rtf', 'COPYING.rtf'),
40 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
41 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
41 ]
42 ]
42
43
43 STAGING_REMOVE_FILES = [
44 STAGING_REMOVE_FILES = [
44 # We use the RTF variant.
45 # We use the RTF variant.
45 'copying.txt',
46 'copying.txt',
46 ]
47 ]
47
48
48 SHORTCUTS = {
49 SHORTCUTS = {
49 # hg.1.html'
50 # hg.1.html'
50 'hg.file.5d3e441c_28d9_5542_afd0_cdd4234f12d5': {
51 'hg.file.5d3e441c_28d9_5542_afd0_cdd4234f12d5': {
51 'Name': 'Mercurial Command Reference',
52 'Name': 'Mercurial Command Reference',
52 },
53 },
53 # hgignore.5.html
54 # hgignore.5.html
54 'hg.file.5757d8e0_f207_5e10_a2ec_3ba0a062f431': {
55 'hg.file.5757d8e0_f207_5e10_a2ec_3ba0a062f431': {
55 'Name': 'Mercurial Ignore Files',
56 'Name': 'Mercurial Ignore Files',
56 },
57 },
57 # hgrc.5.html
58 # hgrc.5.html
58 'hg.file.92e605fd_1d1a_5dc6_9fc0_5d2998eb8f5e': {
59 'hg.file.92e605fd_1d1a_5dc6_9fc0_5d2998eb8f5e': {
59 'Name': 'Mercurial Configuration Files',
60 'Name': 'Mercurial Configuration Files',
60 },
61 },
61 }
62 }
62
63
63
64
64 def find_version(source_dir: pathlib.Path):
65 def find_version(source_dir: pathlib.Path):
65 version_py = source_dir / 'mercurial' / '__version__.py'
66 version_py = source_dir / 'mercurial' / '__version__.py'
66
67
67 with version_py.open('r', encoding='utf-8') as fh:
68 with version_py.open('r', encoding='utf-8') as fh:
68 source = fh.read().strip()
69 source = fh.read().strip()
69
70
70 m = re.search('version = b"(.*)"', source)
71 m = re.search('version = b"(.*)"', source)
71 return m.group(1)
72 return m.group(1)
72
73
73
74
74 def normalize_version(version):
75 """Normalize Mercurial version string so WiX accepts it.
76
77 Version strings have to be numeric ``A.B.C[.D]`` to conform with MSI's
78 requirements.
79
80 We normalize RC version or the commit count to a 4th version component.
81 We store this in the 4th component because ``A.B.C`` releases do occur
82 and we want an e.g. ``5.3rc0`` version to be semantically less than a
83 ``5.3.1rc2`` version. This requires always reserving the 3rd version
84 component for the point release and the ``X.YrcN`` release is always
85 point release 0.
86
87 In the case of an RC and presence of ``+`` suffix data, we can't use both
88 because the version format is limited to 4 components. We choose to use
89 RC and throw away the commit count in the suffix. This means we could
90 produce multiple installers with the same normalized version string.
91
92 >>> normalize_version("5.3")
93 '5.3.0'
94
95 >>> normalize_version("5.3rc0")
96 '5.3.0.0'
97
98 >>> normalize_version("5.3rc1")
99 '5.3.0.1'
100
101 >>> normalize_version("5.3rc1+2-abcdef")
102 '5.3.0.1'
103
104 >>> normalize_version("5.3+2-abcdef")
105 '5.3.0.2'
106 """
107 if '+' in version:
108 version, extra = version.split('+', 1)
109 else:
110 extra = None
111
112 # 4.9rc0
113 if version[:-1].endswith('rc'):
114 rc = int(version[-1:])
115 version = version[:-3]
116 else:
117 rc = None
118
119 # Ensure we have at least X.Y version components.
120 versions = [int(v) for v in version.split('.')]
121 while len(versions) < 3:
122 versions.append(0)
123
124 if len(versions) < 4:
125 if rc is not None:
126 versions.append(rc)
127 elif extra:
128 # <commit count>-<hash>+<date>
129 versions.append(int(extra.split('-')[0]))
130
131 return '.'.join('%d' % x for x in versions[0:4])
132
133
134 def ensure_vc90_merge_modules(build_dir):
75 def ensure_vc90_merge_modules(build_dir):
135 x86 = (
76 x86 = (
136 download_entry(
77 download_entry(
137 'vc9-crt-x86-msm',
78 'vc9-crt-x86-msm',
138 build_dir,
79 build_dir,
139 local_name='microsoft.vcxx.crt.x86_msm.msm',
80 local_name='microsoft.vcxx.crt.x86_msm.msm',
140 )[0],
81 )[0],
141 download_entry(
82 download_entry(
142 'vc9-crt-x86-msm-policy',
83 'vc9-crt-x86-msm-policy',
143 build_dir,
84 build_dir,
144 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm',
85 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm',
145 )[0],
86 )[0],
146 )
87 )
147
88
148 x64 = (
89 x64 = (
149 download_entry(
90 download_entry(
150 'vc9-crt-x64-msm',
91 'vc9-crt-x64-msm',
151 build_dir,
92 build_dir,
152 local_name='microsoft.vcxx.crt.x64_msm.msm',
93 local_name='microsoft.vcxx.crt.x64_msm.msm',
153 )[0],
94 )[0],
154 download_entry(
95 download_entry(
155 'vc9-crt-x64-msm-policy',
96 'vc9-crt-x64-msm-policy',
156 build_dir,
97 build_dir,
157 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm',
98 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm',
158 )[0],
99 )[0],
159 )
100 )
160 return {
101 return {
161 'x86': x86,
102 'x86': x86,
162 'x64': x64,
103 'x64': x64,
163 }
104 }
164
105
165
106
166 def run_candle(wix, cwd, wxs, source_dir, defines=None):
107 def run_candle(wix, cwd, wxs, source_dir, defines=None):
167 args = [
108 args = [
168 str(wix / 'candle.exe'),
109 str(wix / 'candle.exe'),
169 '-nologo',
110 '-nologo',
170 str(wxs),
111 str(wxs),
171 '-dSourceDir=%s' % source_dir,
112 '-dSourceDir=%s' % source_dir,
172 ]
113 ]
173
114
174 if defines:
115 if defines:
175 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
116 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
176
117
177 subprocess.run(args, cwd=str(cwd), check=True)
118 subprocess.run(args, cwd=str(cwd), check=True)
178
119
179
120
180 def make_post_build_signing_fn(
121 def make_post_build_signing_fn(
181 name,
122 name,
182 subject_name=None,
123 subject_name=None,
183 cert_path=None,
124 cert_path=None,
184 cert_password=None,
125 cert_password=None,
185 timestamp_url=None,
126 timestamp_url=None,
186 ):
127 ):
187 """Create a callable that will use signtool to sign hg.exe."""
128 """Create a callable that will use signtool to sign hg.exe."""
188
129
189 def post_build_sign(source_dir, build_dir, dist_dir, version):
130 def post_build_sign(source_dir, build_dir, dist_dir, version):
190 description = '%s %s' % (name, version)
131 description = '%s %s' % (name, version)
191
132
192 sign_with_signtool(
133 sign_with_signtool(
193 dist_dir / 'hg.exe',
134 dist_dir / 'hg.exe',
194 description,
135 description,
195 subject_name=subject_name,
136 subject_name=subject_name,
196 cert_path=cert_path,
137 cert_path=cert_path,
197 cert_password=cert_password,
138 cert_password=cert_password,
198 timestamp_url=timestamp_url,
139 timestamp_url=timestamp_url,
199 )
140 )
200
141
201 return post_build_sign
142 return post_build_sign
202
143
203
144
204 def make_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
145 def make_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
205 """Create XML string listing every file to be installed."""
146 """Create XML string listing every file to be installed."""
206
147
207 # We derive GUIDs from a deterministic file path identifier.
148 # We derive GUIDs from a deterministic file path identifier.
208 # We shoehorn the name into something that looks like a URL because
149 # We shoehorn the name into something that looks like a URL because
209 # the UUID namespaces are supposed to work that way (even though
150 # the UUID namespaces are supposed to work that way (even though
210 # the input data probably is never validated).
151 # the input data probably is never validated).
211
152
212 doc = xml.dom.minidom.parseString(
153 doc = xml.dom.minidom.parseString(
213 '<?xml version="1.0" encoding="utf-8"?>'
154 '<?xml version="1.0" encoding="utf-8"?>'
214 '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">'
155 '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">'
215 '</Wix>'
156 '</Wix>'
216 )
157 )
217
158
218 # Assemble the install layout by directory. This makes it easier to
159 # Assemble the install layout by directory. This makes it easier to
219 # emit XML, since each directory has separate entities.
160 # emit XML, since each directory has separate entities.
220 manifest = collections.defaultdict(dict)
161 manifest = collections.defaultdict(dict)
221
162
222 for root, dirs, files in os.walk(staging_dir):
163 for root, dirs, files in os.walk(staging_dir):
223 dirs.sort()
164 dirs.sort()
224
165
225 root = pathlib.Path(root)
166 root = pathlib.Path(root)
226 rel_dir = root.relative_to(staging_dir)
167 rel_dir = root.relative_to(staging_dir)
227
168
228 for i in range(len(rel_dir.parts)):
169 for i in range(len(rel_dir.parts)):
229 parent = '/'.join(rel_dir.parts[0 : i + 1])
170 parent = '/'.join(rel_dir.parts[0 : i + 1])
230 manifest.setdefault(parent, {})
171 manifest.setdefault(parent, {})
231
172
232 for f in sorted(files):
173 for f in sorted(files):
233 full = root / f
174 full = root / f
234 manifest[str(rel_dir).replace('\\', '/')][full.name] = full
175 manifest[str(rel_dir).replace('\\', '/')][full.name] = full
235
176
236 component_groups = collections.defaultdict(list)
177 component_groups = collections.defaultdict(list)
237
178
238 # Now emit a <Fragment> for each directory.
179 # Now emit a <Fragment> for each directory.
239 # Each directory is composed of a <DirectoryRef> pointing to its parent
180 # Each directory is composed of a <DirectoryRef> pointing to its parent
240 # and defines child <Directory>'s and a <Component> with all the files.
181 # and defines child <Directory>'s and a <Component> with all the files.
241 for dir_name, entries in sorted(manifest.items()):
182 for dir_name, entries in sorted(manifest.items()):
242 # The directory id is derived from the path. But the root directory
183 # The directory id is derived from the path. But the root directory
243 # is special.
184 # is special.
244 if dir_name == '.':
185 if dir_name == '.':
245 parent_directory_id = 'INSTALLDIR'
186 parent_directory_id = 'INSTALLDIR'
246 else:
187 else:
247 parent_directory_id = 'hg.dir.%s' % dir_name.replace('/', '.')
188 parent_directory_id = 'hg.dir.%s' % dir_name.replace('/', '.')
248
189
249 fragment = doc.createElement('Fragment')
190 fragment = doc.createElement('Fragment')
250 directory_ref = doc.createElement('DirectoryRef')
191 directory_ref = doc.createElement('DirectoryRef')
251 directory_ref.setAttribute('Id', parent_directory_id)
192 directory_ref.setAttribute('Id', parent_directory_id)
252
193
253 # Add <Directory> entries for immediate children directories.
194 # Add <Directory> entries for immediate children directories.
254 for possible_child in sorted(manifest.keys()):
195 for possible_child in sorted(manifest.keys()):
255 if (
196 if (
256 dir_name == '.'
197 dir_name == '.'
257 and '/' not in possible_child
198 and '/' not in possible_child
258 and possible_child != '.'
199 and possible_child != '.'
259 ):
200 ):
260 child_directory_id = 'hg.dir.%s' % possible_child
201 child_directory_id = 'hg.dir.%s' % possible_child
261 name = possible_child
202 name = possible_child
262 else:
203 else:
263 if not possible_child.startswith('%s/' % dir_name):
204 if not possible_child.startswith('%s/' % dir_name):
264 continue
205 continue
265 name = possible_child[len(dir_name) + 1 :]
206 name = possible_child[len(dir_name) + 1 :]
266 if '/' in name:
207 if '/' in name:
267 continue
208 continue
268
209
269 child_directory_id = 'hg.dir.%s' % possible_child.replace(
210 child_directory_id = 'hg.dir.%s' % possible_child.replace(
270 '/', '.'
211 '/', '.'
271 )
212 )
272
213
273 directory = doc.createElement('Directory')
214 directory = doc.createElement('Directory')
274 directory.setAttribute('Id', child_directory_id)
215 directory.setAttribute('Id', child_directory_id)
275 directory.setAttribute('Name', name)
216 directory.setAttribute('Name', name)
276 directory_ref.appendChild(directory)
217 directory_ref.appendChild(directory)
277
218
278 # Add <Component>s for files in this directory.
219 # Add <Component>s for files in this directory.
279 for rel, source_path in sorted(entries.items()):
220 for rel, source_path in sorted(entries.items()):
280 if dir_name == '.':
221 if dir_name == '.':
281 full_rel = rel
222 full_rel = rel
282 else:
223 else:
283 full_rel = '%s/%s' % (dir_name, rel)
224 full_rel = '%s/%s' % (dir_name, rel)
284
225
285 component_unique_id = (
226 component_unique_id = (
286 'https://www.mercurial-scm.org/wix-installer/0/component/%s'
227 'https://www.mercurial-scm.org/wix-installer/0/component/%s'
287 % full_rel
228 % full_rel
288 )
229 )
289 component_guid = uuid.uuid5(uuid.NAMESPACE_URL, component_unique_id)
230 component_guid = uuid.uuid5(uuid.NAMESPACE_URL, component_unique_id)
290 component_id = 'hg.component.%s' % str(component_guid).replace(
231 component_id = 'hg.component.%s' % str(component_guid).replace(
291 '-', '_'
232 '-', '_'
292 )
233 )
293
234
294 component = doc.createElement('Component')
235 component = doc.createElement('Component')
295
236
296 component.setAttribute('Id', component_id)
237 component.setAttribute('Id', component_id)
297 component.setAttribute('Guid', str(component_guid).upper())
238 component.setAttribute('Guid', str(component_guid).upper())
298 component.setAttribute('Win64', 'yes' if is_x64 else 'no')
239 component.setAttribute('Win64', 'yes' if is_x64 else 'no')
299
240
300 # Assign this component to a top-level group.
241 # Assign this component to a top-level group.
301 if dir_name == '.':
242 if dir_name == '.':
302 component_groups['ROOT'].append(component_id)
243 component_groups['ROOT'].append(component_id)
303 elif '/' in dir_name:
244 elif '/' in dir_name:
304 component_groups[dir_name[0 : dir_name.index('/')]].append(
245 component_groups[dir_name[0 : dir_name.index('/')]].append(
305 component_id
246 component_id
306 )
247 )
307 else:
248 else:
308 component_groups[dir_name].append(component_id)
249 component_groups[dir_name].append(component_id)
309
250
310 unique_id = (
251 unique_id = (
311 'https://www.mercurial-scm.org/wix-installer/0/%s' % full_rel
252 'https://www.mercurial-scm.org/wix-installer/0/%s' % full_rel
312 )
253 )
313 file_guid = uuid.uuid5(uuid.NAMESPACE_URL, unique_id)
254 file_guid = uuid.uuid5(uuid.NAMESPACE_URL, unique_id)
314
255
315 # IDs have length limits. So use GUID to derive them.
256 # IDs have length limits. So use GUID to derive them.
316 file_guid_normalized = str(file_guid).replace('-', '_')
257 file_guid_normalized = str(file_guid).replace('-', '_')
317 file_id = 'hg.file.%s' % file_guid_normalized
258 file_id = 'hg.file.%s' % file_guid_normalized
318
259
319 file_element = doc.createElement('File')
260 file_element = doc.createElement('File')
320 file_element.setAttribute('Id', file_id)
261 file_element.setAttribute('Id', file_id)
321 file_element.setAttribute('Source', str(source_path))
262 file_element.setAttribute('Source', str(source_path))
322 file_element.setAttribute('KeyPath', 'yes')
263 file_element.setAttribute('KeyPath', 'yes')
323 file_element.setAttribute('ReadOnly', 'yes')
264 file_element.setAttribute('ReadOnly', 'yes')
324
265
325 component.appendChild(file_element)
266 component.appendChild(file_element)
326 directory_ref.appendChild(component)
267 directory_ref.appendChild(component)
327
268
328 fragment.appendChild(directory_ref)
269 fragment.appendChild(directory_ref)
329 doc.documentElement.appendChild(fragment)
270 doc.documentElement.appendChild(fragment)
330
271
331 for group, component_ids in sorted(component_groups.items()):
272 for group, component_ids in sorted(component_groups.items()):
332 fragment = doc.createElement('Fragment')
273 fragment = doc.createElement('Fragment')
333 component_group = doc.createElement('ComponentGroup')
274 component_group = doc.createElement('ComponentGroup')
334 component_group.setAttribute('Id', 'hg.group.%s' % group)
275 component_group.setAttribute('Id', 'hg.group.%s' % group)
335
276
336 for component_id in component_ids:
277 for component_id in component_ids:
337 component_ref = doc.createElement('ComponentRef')
278 component_ref = doc.createElement('ComponentRef')
338 component_ref.setAttribute('Id', component_id)
279 component_ref.setAttribute('Id', component_id)
339 component_group.appendChild(component_ref)
280 component_group.appendChild(component_ref)
340
281
341 fragment.appendChild(component_group)
282 fragment.appendChild(component_group)
342 doc.documentElement.appendChild(fragment)
283 doc.documentElement.appendChild(fragment)
343
284
344 # Add <Shortcut> to files that have it defined.
285 # Add <Shortcut> to files that have it defined.
345 for file_id, metadata in sorted(SHORTCUTS.items()):
286 for file_id, metadata in sorted(SHORTCUTS.items()):
346 els = doc.getElementsByTagName('File')
287 els = doc.getElementsByTagName('File')
347 els = [el for el in els if el.getAttribute('Id') == file_id]
288 els = [el for el in els if el.getAttribute('Id') == file_id]
348
289
349 if not els:
290 if not els:
350 raise Exception('could not find File[Id=%s]' % file_id)
291 raise Exception('could not find File[Id=%s]' % file_id)
351
292
352 for el in els:
293 for el in els:
353 shortcut = doc.createElement('Shortcut')
294 shortcut = doc.createElement('Shortcut')
354 shortcut.setAttribute('Id', 'hg.shortcut.%s' % file_id)
295 shortcut.setAttribute('Id', 'hg.shortcut.%s' % file_id)
355 shortcut.setAttribute('Directory', 'ProgramMenuDir')
296 shortcut.setAttribute('Directory', 'ProgramMenuDir')
356 shortcut.setAttribute('Icon', 'hgIcon.ico')
297 shortcut.setAttribute('Icon', 'hgIcon.ico')
357 shortcut.setAttribute('IconIndex', '0')
298 shortcut.setAttribute('IconIndex', '0')
358 shortcut.setAttribute('Advertise', 'yes')
299 shortcut.setAttribute('Advertise', 'yes')
359 for k, v in sorted(metadata.items()):
300 for k, v in sorted(metadata.items()):
360 shortcut.setAttribute(k, v)
301 shortcut.setAttribute(k, v)
361
302
362 el.appendChild(shortcut)
303 el.appendChild(shortcut)
363
304
364 return doc.toprettyxml()
305 return doc.toprettyxml()
365
306
366
307
367 def build_installer(
308 def build_installer(
368 source_dir: pathlib.Path,
309 source_dir: pathlib.Path,
369 python_exe: pathlib.Path,
310 python_exe: pathlib.Path,
370 msi_name='mercurial',
311 msi_name='mercurial',
371 version=None,
312 version=None,
372 post_build_fn=None,
313 post_build_fn=None,
373 extra_packages_script=None,
314 extra_packages_script=None,
374 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
315 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
375 extra_features: typing.Optional[typing.List[str]] = None,
316 extra_features: typing.Optional[typing.List[str]] = None,
376 ):
317 ):
377 """Build a WiX MSI installer.
318 """Build a WiX MSI installer.
378
319
379 ``source_dir`` is the path to the Mercurial source tree to use.
320 ``source_dir`` is the path to the Mercurial source tree to use.
380 ``arch`` is the target architecture. either ``x86`` or ``x64``.
321 ``arch`` is the target architecture. either ``x86`` or ``x64``.
381 ``python_exe`` is the path to the Python executable to use/bundle.
322 ``python_exe`` is the path to the Python executable to use/bundle.
382 ``version`` is the Mercurial version string. If not defined,
323 ``version`` is the Mercurial version string. If not defined,
383 ``mercurial/__version__.py`` will be consulted.
324 ``mercurial/__version__.py`` will be consulted.
384 ``post_build_fn`` is a callable that will be called after building
325 ``post_build_fn`` is a callable that will be called after building
385 Mercurial but before invoking WiX. It can be used to e.g. facilitate
326 Mercurial but before invoking WiX. It can be used to e.g. facilitate
386 signing. It is passed the paths to the Mercurial source, build, and
327 signing. It is passed the paths to the Mercurial source, build, and
387 dist directories and the resolved Mercurial version.
328 dist directories and the resolved Mercurial version.
388 ``extra_packages_script`` is a command to be run to inject extra packages
329 ``extra_packages_script`` is a command to be run to inject extra packages
389 into the py2exe binary. It should stage packages into the virtualenv and
330 into the py2exe binary. It should stage packages into the virtualenv and
390 print a null byte followed by a newline-separated list of packages that
331 print a null byte followed by a newline-separated list of packages that
391 should be included in the exe.
332 should be included in the exe.
392 ``extra_wxs`` is a dict of {wxs_name: working_dir_for_wxs_build}.
333 ``extra_wxs`` is a dict of {wxs_name: working_dir_for_wxs_build}.
393 ``extra_features`` is a list of additional named Features to include in
334 ``extra_features`` is a list of additional named Features to include in
394 the build. These must match Feature names in one of the wxs scripts.
335 the build. These must match Feature names in one of the wxs scripts.
395 """
336 """
396 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
337 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
397
338
398 hg_build_dir = source_dir / 'build'
339 hg_build_dir = source_dir / 'build'
399 dist_dir = source_dir / 'dist'
340 dist_dir = source_dir / 'dist'
400 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
341 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
401
342
402 requirements_txt = wix_dir / 'requirements.txt'
343 requirements_txt = wix_dir / 'requirements.txt'
403
344
404 build_py2exe(
345 build_py2exe(
405 source_dir,
346 source_dir,
406 hg_build_dir,
347 hg_build_dir,
407 python_exe,
348 python_exe,
408 'wix',
349 'wix',
409 requirements_txt,
350 requirements_txt,
410 extra_packages=EXTRA_PACKAGES,
351 extra_packages=EXTRA_PACKAGES,
411 extra_packages_script=extra_packages_script,
352 extra_packages_script=extra_packages_script,
412 )
353 )
413
354
414 orig_version = version or find_version(source_dir)
355 orig_version = version or find_version(source_dir)
415 version = normalize_version(orig_version)
356 version = normalize_windows_version(orig_version)
416 print('using version string: %s' % version)
357 print('using version string: %s' % version)
417 if version != orig_version:
358 if version != orig_version:
418 print('(normalized from: %s)' % orig_version)
359 print('(normalized from: %s)' % orig_version)
419
360
420 if post_build_fn:
361 if post_build_fn:
421 post_build_fn(source_dir, hg_build_dir, dist_dir, version)
362 post_build_fn(source_dir, hg_build_dir, dist_dir, version)
422
363
423 build_dir = hg_build_dir / ('wix-%s' % arch)
364 build_dir = hg_build_dir / ('wix-%s' % arch)
424 staging_dir = build_dir / 'stage'
365 staging_dir = build_dir / 'stage'
425
366
426 build_dir.mkdir(exist_ok=True)
367 build_dir.mkdir(exist_ok=True)
427
368
428 # Purge the staging directory for every build so packaging is pristine.
369 # Purge the staging directory for every build so packaging is pristine.
429 if staging_dir.exists():
370 if staging_dir.exists():
430 print('purging %s' % staging_dir)
371 print('purging %s' % staging_dir)
431 shutil.rmtree(staging_dir)
372 shutil.rmtree(staging_dir)
432
373
433 stage_install(source_dir, staging_dir, lower_case=True)
374 stage_install(source_dir, staging_dir, lower_case=True)
434
375
435 # We also install some extra files.
376 # We also install some extra files.
436 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
377 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
437
378
438 # And remove some files we don't want.
379 # And remove some files we don't want.
439 for f in STAGING_REMOVE_FILES:
380 for f in STAGING_REMOVE_FILES:
440 p = staging_dir / f
381 p = staging_dir / f
441 if p.exists():
382 if p.exists():
442 print('removing %s' % p)
383 print('removing %s' % p)
443 p.unlink()
384 p.unlink()
444
385
445 wix_pkg, wix_entry = download_entry('wix', hg_build_dir)
386 wix_pkg, wix_entry = download_entry('wix', hg_build_dir)
446 wix_path = hg_build_dir / ('wix-%s' % wix_entry['version'])
387 wix_path = hg_build_dir / ('wix-%s' % wix_entry['version'])
447
388
448 if not wix_path.exists():
389 if not wix_path.exists():
449 extract_zip_to_directory(wix_pkg, wix_path)
390 extract_zip_to_directory(wix_pkg, wix_path)
450
391
451 ensure_vc90_merge_modules(hg_build_dir)
392 ensure_vc90_merge_modules(hg_build_dir)
452
393
453 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
394 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
454
395
455 defines = {'Platform': arch}
396 defines = {'Platform': arch}
456
397
457 # Derive a .wxs file with the staged files.
398 # Derive a .wxs file with the staged files.
458 manifest_wxs = build_dir / 'stage.wxs'
399 manifest_wxs = build_dir / 'stage.wxs'
459 with manifest_wxs.open('w', encoding='utf-8') as fh:
400 with manifest_wxs.open('w', encoding='utf-8') as fh:
460 fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
401 fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
461
402
462 run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
403 run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
463
404
464 for source, rel_path in sorted((extra_wxs or {}).items()):
405 for source, rel_path in sorted((extra_wxs or {}).items()):
465 run_candle(wix_path, build_dir, source, rel_path, defines=defines)
406 run_candle(wix_path, build_dir, source, rel_path, defines=defines)
466
407
467 source = wix_dir / 'mercurial.wxs'
408 source = wix_dir / 'mercurial.wxs'
468 defines['Version'] = version
409 defines['Version'] = version
469 defines['Comments'] = 'Installs Mercurial version %s' % version
410 defines['Comments'] = 'Installs Mercurial version %s' % version
470 defines['VCRedistSrcDir'] = str(hg_build_dir)
411 defines['VCRedistSrcDir'] = str(hg_build_dir)
471 if extra_features:
412 if extra_features:
472 assert all(';' not in f for f in extra_features)
413 assert all(';' not in f for f in extra_features)
473 defines['MercurialExtraFeatures'] = ';'.join(extra_features)
414 defines['MercurialExtraFeatures'] = ';'.join(extra_features)
474
415
475 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
416 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
476
417
477 msi_path = (
418 msi_path = (
478 source_dir / 'dist' / ('%s-%s-%s.msi' % (msi_name, orig_version, arch))
419 source_dir / 'dist' / ('%s-%s-%s.msi' % (msi_name, orig_version, arch))
479 )
420 )
480
421
481 args = [
422 args = [
482 str(wix_path / 'light.exe'),
423 str(wix_path / 'light.exe'),
483 '-nologo',
424 '-nologo',
484 '-ext',
425 '-ext',
485 'WixUIExtension',
426 'WixUIExtension',
486 '-sw1076',
427 '-sw1076',
487 '-spdb',
428 '-spdb',
488 '-o',
429 '-o',
489 str(msi_path),
430 str(msi_path),
490 ]
431 ]
491
432
492 for source, rel_path in sorted((extra_wxs or {}).items()):
433 for source, rel_path in sorted((extra_wxs or {}).items()):
493 assert source.endswith('.wxs')
434 assert source.endswith('.wxs')
494 source = os.path.basename(source)
435 source = os.path.basename(source)
495 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
436 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
496
437
497 args.extend(
438 args.extend(
498 [str(build_dir / 'stage.wixobj'), str(build_dir / 'mercurial.wixobj'),]
439 [str(build_dir / 'stage.wixobj'), str(build_dir / 'mercurial.wixobj'),]
499 )
440 )
500
441
501 subprocess.run(args, cwd=str(source_dir), check=True)
442 subprocess.run(args, cwd=str(source_dir), check=True)
502
443
503 print('%s created' % msi_path)
444 print('%s created' % msi_path)
504
445
505 return {
446 return {
506 'msi_path': msi_path,
447 'msi_path': msi_path,
507 }
448 }
508
449
509
450
510 def build_signed_installer(
451 def build_signed_installer(
511 source_dir: pathlib.Path,
452 source_dir: pathlib.Path,
512 python_exe: pathlib.Path,
453 python_exe: pathlib.Path,
513 name: str,
454 name: str,
514 version=None,
455 version=None,
515 subject_name=None,
456 subject_name=None,
516 cert_path=None,
457 cert_path=None,
517 cert_password=None,
458 cert_password=None,
518 timestamp_url=None,
459 timestamp_url=None,
519 extra_packages_script=None,
460 extra_packages_script=None,
520 extra_wxs=None,
461 extra_wxs=None,
521 extra_features=None,
462 extra_features=None,
522 ):
463 ):
523 """Build an installer with signed executables."""
464 """Build an installer with signed executables."""
524
465
525 post_build_fn = make_post_build_signing_fn(
466 post_build_fn = make_post_build_signing_fn(
526 name,
467 name,
527 subject_name=subject_name,
468 subject_name=subject_name,
528 cert_path=cert_path,
469 cert_path=cert_path,
529 cert_password=cert_password,
470 cert_password=cert_password,
530 timestamp_url=timestamp_url,
471 timestamp_url=timestamp_url,
531 )
472 )
532
473
533 info = build_installer(
474 info = build_installer(
534 source_dir,
475 source_dir,
535 python_exe=python_exe,
476 python_exe=python_exe,
536 msi_name=name.lower(),
477 msi_name=name.lower(),
537 version=version,
478 version=version,
538 post_build_fn=post_build_fn,
479 post_build_fn=post_build_fn,
539 extra_packages_script=extra_packages_script,
480 extra_packages_script=extra_packages_script,
540 extra_wxs=extra_wxs,
481 extra_wxs=extra_wxs,
541 extra_features=extra_features,
482 extra_features=extra_features,
542 )
483 )
543
484
544 description = '%s %s' % (name, version)
485 description = '%s %s' % (name, version)
545
486
546 sign_with_signtool(
487 sign_with_signtool(
547 info['msi_path'],
488 info['msi_path'],
548 description,
489 description,
549 subject_name=subject_name,
490 subject_name=subject_name,
550 cert_path=cert_path,
491 cert_path=cert_path,
551 cert_password=cert_password,
492 cert_password=cert_password,
552 timestamp_url=timestamp_url,
493 timestamp_url=timestamp_url,
553 )
494 )
General Comments 0
You need to be logged in to leave comments. Login now