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