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