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