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