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