##// END OF EJS Templates
packaging: make the path to Win32 requirements absolute when building WiX...
Matt Harbison -
r44724:847e582f stable
parent child Browse files
Show More
@@ -1,497 +1,499 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'
344 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
345
345
346 requirements_txt = 'requirements_win32.txt'
346 requirements_txt = (
347 source_dir / 'contrib' / 'packaging' / 'requirements_win32.txt'
348 )
347
349
348 build_py2exe(
350 build_py2exe(
349 source_dir,
351 source_dir,
350 hg_build_dir,
352 hg_build_dir,
351 python_exe,
353 python_exe,
352 'wix',
354 'wix',
353 requirements_txt,
355 requirements_txt,
354 extra_packages=EXTRA_PACKAGES,
356 extra_packages=EXTRA_PACKAGES,
355 extra_packages_script=extra_packages_script,
357 extra_packages_script=extra_packages_script,
356 )
358 )
357
359
358 orig_version = version or find_version(source_dir)
360 orig_version = version or find_version(source_dir)
359 version = normalize_windows_version(orig_version)
361 version = normalize_windows_version(orig_version)
360 print('using version string: %s' % version)
362 print('using version string: %s' % version)
361 if version != orig_version:
363 if version != orig_version:
362 print('(normalized from: %s)' % orig_version)
364 print('(normalized from: %s)' % orig_version)
363
365
364 if post_build_fn:
366 if post_build_fn:
365 post_build_fn(source_dir, hg_build_dir, dist_dir, version)
367 post_build_fn(source_dir, hg_build_dir, dist_dir, version)
366
368
367 build_dir = hg_build_dir / ('wix-%s' % arch)
369 build_dir = hg_build_dir / ('wix-%s' % arch)
368 staging_dir = build_dir / 'stage'
370 staging_dir = build_dir / 'stage'
369
371
370 build_dir.mkdir(exist_ok=True)
372 build_dir.mkdir(exist_ok=True)
371
373
372 # Purge the staging directory for every build so packaging is pristine.
374 # Purge the staging directory for every build so packaging is pristine.
373 if staging_dir.exists():
375 if staging_dir.exists():
374 print('purging %s' % staging_dir)
376 print('purging %s' % staging_dir)
375 shutil.rmtree(staging_dir)
377 shutil.rmtree(staging_dir)
376
378
377 stage_install(source_dir, staging_dir, lower_case=True)
379 stage_install(source_dir, staging_dir, lower_case=True)
378
380
379 # We also install some extra files.
381 # We also install some extra files.
380 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
382 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
381
383
382 # And remove some files we don't want.
384 # And remove some files we don't want.
383 for f in STAGING_REMOVE_FILES:
385 for f in STAGING_REMOVE_FILES:
384 p = staging_dir / f
386 p = staging_dir / f
385 if p.exists():
387 if p.exists():
386 print('removing %s' % p)
388 print('removing %s' % p)
387 p.unlink()
389 p.unlink()
388
390
389 wix_pkg, wix_entry = download_entry('wix', hg_build_dir)
391 wix_pkg, wix_entry = download_entry('wix', hg_build_dir)
390 wix_path = hg_build_dir / ('wix-%s' % wix_entry['version'])
392 wix_path = hg_build_dir / ('wix-%s' % wix_entry['version'])
391
393
392 if not wix_path.exists():
394 if not wix_path.exists():
393 extract_zip_to_directory(wix_pkg, wix_path)
395 extract_zip_to_directory(wix_pkg, wix_path)
394
396
395 ensure_vc90_merge_modules(hg_build_dir)
397 ensure_vc90_merge_modules(hg_build_dir)
396
398
397 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
399 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
398
400
399 defines = {'Platform': arch}
401 defines = {'Platform': arch}
400
402
401 # Derive a .wxs file with the staged files.
403 # Derive a .wxs file with the staged files.
402 manifest_wxs = build_dir / 'stage.wxs'
404 manifest_wxs = build_dir / 'stage.wxs'
403 with manifest_wxs.open('w', encoding='utf-8') as fh:
405 with manifest_wxs.open('w', encoding='utf-8') as fh:
404 fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
406 fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
405
407
406 run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
408 run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
407
409
408 for source, rel_path in sorted((extra_wxs or {}).items()):
410 for source, rel_path in sorted((extra_wxs or {}).items()):
409 run_candle(wix_path, build_dir, source, rel_path, defines=defines)
411 run_candle(wix_path, build_dir, source, rel_path, defines=defines)
410
412
411 source = wix_dir / 'mercurial.wxs'
413 source = wix_dir / 'mercurial.wxs'
412 defines['Version'] = version
414 defines['Version'] = version
413 defines['Comments'] = 'Installs Mercurial version %s' % version
415 defines['Comments'] = 'Installs Mercurial version %s' % version
414 defines['VCRedistSrcDir'] = str(hg_build_dir)
416 defines['VCRedistSrcDir'] = str(hg_build_dir)
415 if extra_features:
417 if extra_features:
416 assert all(';' not in f for f in extra_features)
418 assert all(';' not in f for f in extra_features)
417 defines['MercurialExtraFeatures'] = ';'.join(extra_features)
419 defines['MercurialExtraFeatures'] = ';'.join(extra_features)
418
420
419 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
421 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
420
422
421 msi_path = (
423 msi_path = (
422 source_dir / 'dist' / ('%s-%s-%s.msi' % (msi_name, orig_version, arch))
424 source_dir / 'dist' / ('%s-%s-%s.msi' % (msi_name, orig_version, arch))
423 )
425 )
424
426
425 args = [
427 args = [
426 str(wix_path / 'light.exe'),
428 str(wix_path / 'light.exe'),
427 '-nologo',
429 '-nologo',
428 '-ext',
430 '-ext',
429 'WixUIExtension',
431 'WixUIExtension',
430 '-sw1076',
432 '-sw1076',
431 '-spdb',
433 '-spdb',
432 '-o',
434 '-o',
433 str(msi_path),
435 str(msi_path),
434 ]
436 ]
435
437
436 for source, rel_path in sorted((extra_wxs or {}).items()):
438 for source, rel_path in sorted((extra_wxs or {}).items()):
437 assert source.endswith('.wxs')
439 assert source.endswith('.wxs')
438 source = os.path.basename(source)
440 source = os.path.basename(source)
439 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
441 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
440
442
441 args.extend(
443 args.extend(
442 [str(build_dir / 'stage.wixobj'), str(build_dir / 'mercurial.wixobj'),]
444 [str(build_dir / 'stage.wixobj'), str(build_dir / 'mercurial.wixobj'),]
443 )
445 )
444
446
445 subprocess.run(args, cwd=str(source_dir), check=True)
447 subprocess.run(args, cwd=str(source_dir), check=True)
446
448
447 print('%s created' % msi_path)
449 print('%s created' % msi_path)
448
450
449 return {
451 return {
450 'msi_path': msi_path,
452 'msi_path': msi_path,
451 }
453 }
452
454
453
455
454 def build_signed_installer(
456 def build_signed_installer(
455 source_dir: pathlib.Path,
457 source_dir: pathlib.Path,
456 python_exe: pathlib.Path,
458 python_exe: pathlib.Path,
457 name: str,
459 name: str,
458 version=None,
460 version=None,
459 subject_name=None,
461 subject_name=None,
460 cert_path=None,
462 cert_path=None,
461 cert_password=None,
463 cert_password=None,
462 timestamp_url=None,
464 timestamp_url=None,
463 extra_packages_script=None,
465 extra_packages_script=None,
464 extra_wxs=None,
466 extra_wxs=None,
465 extra_features=None,
467 extra_features=None,
466 ):
468 ):
467 """Build an installer with signed executables."""
469 """Build an installer with signed executables."""
468
470
469 post_build_fn = make_post_build_signing_fn(
471 post_build_fn = make_post_build_signing_fn(
470 name,
472 name,
471 subject_name=subject_name,
473 subject_name=subject_name,
472 cert_path=cert_path,
474 cert_path=cert_path,
473 cert_password=cert_password,
475 cert_password=cert_password,
474 timestamp_url=timestamp_url,
476 timestamp_url=timestamp_url,
475 )
477 )
476
478
477 info = build_installer(
479 info = build_installer(
478 source_dir,
480 source_dir,
479 python_exe=python_exe,
481 python_exe=python_exe,
480 msi_name=name.lower(),
482 msi_name=name.lower(),
481 version=version,
483 version=version,
482 post_build_fn=post_build_fn,
484 post_build_fn=post_build_fn,
483 extra_packages_script=extra_packages_script,
485 extra_packages_script=extra_packages_script,
484 extra_wxs=extra_wxs,
486 extra_wxs=extra_wxs,
485 extra_features=extra_features,
487 extra_features=extra_features,
486 )
488 )
487
489
488 description = '%s %s' % (name, version)
490 description = '%s %s' % (name, version)
489
491
490 sign_with_signtool(
492 sign_with_signtool(
491 info['msi_path'],
493 info['msi_path'],
492 description,
494 description,
493 subject_name=subject_name,
495 subject_name=subject_name,
494 cert_path=cert_path,
496 cert_path=cert_path,
495 cert_password=cert_password,
497 cert_password=cert_password,
496 timestamp_url=timestamp_url,
498 timestamp_url=timestamp_url,
497 )
499 )
General Comments 0
You need to be logged in to leave comments. Login now