##// END OF EJS Templates
packaging: add -python2 to Windows installer filenames...
Gregory Szorc -
r45276:9ade217b stable
parent child Browse files
Show More
@@ -1,533 +1,533 b''
1 1 # windows.py - Automation specific to Windows
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 datetime
11 11 import os
12 12 import paramiko
13 13 import pathlib
14 14 import re
15 15 import subprocess
16 16 import tempfile
17 17
18 18 from .pypi import upload as pypi_upload
19 19 from .winrm import run_powershell
20 20
21 21
22 22 # PowerShell commands to activate a Visual Studio 2008 environment.
23 23 # This is essentially a port of vcvarsall.bat to PowerShell.
24 24 ACTIVATE_VC9_AMD64 = r'''
25 25 Write-Output "activating Visual Studio 2008 environment for AMD64"
26 26 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
27 27 $Env:VCINSTALLDIR = "${root}\VC\"
28 28 $Env:WindowsSdkDir = "${root}\WinSDK\"
29 29 $Env:PATH = "${root}\VC\Bin\amd64;${root}\WinSDK\Bin\x64;${root}\WinSDK\Bin;$Env:PATH"
30 30 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:PATH"
31 31 $Env:LIB = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIB"
32 32 $Env:LIBPATH = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIBPATH"
33 33 '''.lstrip()
34 34
35 35 ACTIVATE_VC9_X86 = r'''
36 36 Write-Output "activating Visual Studio 2008 environment for x86"
37 37 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
38 38 $Env:VCINSTALLDIR = "${root}\VC\"
39 39 $Env:WindowsSdkDir = "${root}\WinSDK\"
40 40 $Env:PATH = "${root}\VC\Bin;${root}\WinSDK\Bin;$Env:PATH"
41 41 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:INCLUDE"
42 42 $Env:LIB = "${root}\VC\Lib;${root}\WinSDK\Lib;$Env:LIB"
43 43 $Env:LIBPATH = "${root}\VC\lib;${root}\WinSDK\Lib;$Env:LIBPATH"
44 44 '''.lstrip()
45 45
46 46 HG_PURGE = r'''
47 47 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
48 48 Set-Location C:\hgdev\src
49 49 hg.exe --config extensions.purge= purge --all
50 50 if ($LASTEXITCODE -ne 0) {
51 51 throw "process exited non-0: $LASTEXITCODE"
52 52 }
53 53 Write-Output "purged Mercurial repo"
54 54 '''
55 55
56 56 HG_UPDATE_CLEAN = r'''
57 57 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
58 58 Set-Location C:\hgdev\src
59 59 hg.exe --config extensions.purge= purge --all
60 60 if ($LASTEXITCODE -ne 0) {{
61 61 throw "process exited non-0: $LASTEXITCODE"
62 62 }}
63 63 hg.exe update -C {revision}
64 64 if ($LASTEXITCODE -ne 0) {{
65 65 throw "process exited non-0: $LASTEXITCODE"
66 66 }}
67 67 hg.exe log -r .
68 68 Write-Output "updated Mercurial working directory to {revision}"
69 69 '''.lstrip()
70 70
71 71 BUILD_INNO = r'''
72 72 Set-Location C:\hgdev\src
73 73 $python = "C:\hgdev\python27-{arch}\python.exe"
74 74 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py inno --python $python
75 75 if ($LASTEXITCODE -ne 0) {{
76 76 throw "process exited non-0: $LASTEXITCODE"
77 77 }}
78 78 '''.lstrip()
79 79
80 80 BUILD_WHEEL = r'''
81 81 Set-Location C:\hgdev\src
82 82 C:\hgdev\python{python_version}-{arch}\python.exe -m pip wheel --wheel-dir dist .
83 83 if ($LASTEXITCODE -ne 0) {{
84 84 throw "process exited non-0: $LASTEXITCODE"
85 85 }}
86 86 '''
87 87
88 88 BUILD_WIX = r'''
89 89 Set-Location C:\hgdev\src
90 90 $python = "C:\hgdev\python27-{arch}\python.exe"
91 91 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py wix --python $python {extra_args}
92 92 if ($LASTEXITCODE -ne 0) {{
93 93 throw "process exited non-0: $LASTEXITCODE"
94 94 }}
95 95 '''
96 96
97 97 RUN_TESTS = r'''
98 98 C:\hgdev\MinGW\msys\1.0\bin\sh.exe --login -c "cd /c/hgdev/src/tests && /c/hgdev/{python_path}/python.exe run-tests.py {test_flags}"
99 99 if ($LASTEXITCODE -ne 0) {{
100 100 throw "process exited non-0: $LASTEXITCODE"
101 101 }}
102 102 '''
103 103
104 104 WHEEL_FILENAME_PYTHON27_X86 = 'mercurial-{version}-cp27-cp27m-win32.whl'
105 105 WHEEL_FILENAME_PYTHON27_X64 = 'mercurial-{version}-cp27-cp27m-win_amd64.whl'
106 106 WHEEL_FILENAME_PYTHON37_X86 = 'mercurial-{version}-cp37-cp37m-win32.whl'
107 107 WHEEL_FILENAME_PYTHON37_X64 = 'mercurial-{version}-cp37-cp37m-win_amd64.whl'
108 108 WHEEL_FILENAME_PYTHON38_X86 = 'mercurial-{version}-cp38-cp38-win32.whl'
109 109 WHEEL_FILENAME_PYTHON38_X64 = 'mercurial-{version}-cp38-cp38-win_amd64.whl'
110 110
111 X86_EXE_FILENAME = 'Mercurial-{version}.exe'
112 X64_EXE_FILENAME = 'Mercurial-{version}-x64.exe'
113 X86_MSI_FILENAME = 'mercurial-{version}-x86.msi'
114 X64_MSI_FILENAME = 'mercurial-{version}-x64.msi'
111 X86_EXE_FILENAME = 'Mercurial-{version}-x86-python2.exe'
112 X64_EXE_FILENAME = 'Mercurial-{version}-x64-python2.exe'
113 X86_MSI_FILENAME = 'mercurial-{version}-x86-python2.msi'
114 X64_MSI_FILENAME = 'mercurial-{version}-x64-python2.msi'
115 115
116 116 MERCURIAL_SCM_BASE_URL = 'https://mercurial-scm.org/release/windows'
117 117
118 118 X86_USER_AGENT_PATTERN = '.*Windows.*'
119 119 X64_USER_AGENT_PATTERN = '.*Windows.*(WOW|x)64.*'
120 120
121 121 X86_EXE_DESCRIPTION = (
122 122 'Mercurial {version} Inno Setup installer - x86 Windows '
123 123 '- does not require admin rights'
124 124 )
125 125 X64_EXE_DESCRIPTION = (
126 126 'Mercurial {version} Inno Setup installer - x64 Windows '
127 127 '- does not require admin rights'
128 128 )
129 129 X86_MSI_DESCRIPTION = (
130 130 'Mercurial {version} MSI installer - x86 Windows ' '- requires admin rights'
131 131 )
132 132 X64_MSI_DESCRIPTION = (
133 133 'Mercurial {version} MSI installer - x64 Windows ' '- requires admin rights'
134 134 )
135 135
136 136
137 137 def get_vc_prefix(arch):
138 138 if arch == 'x86':
139 139 return ACTIVATE_VC9_X86
140 140 elif arch == 'x64':
141 141 return ACTIVATE_VC9_AMD64
142 142 else:
143 143 raise ValueError('illegal arch: %s; must be x86 or x64' % arch)
144 144
145 145
146 146 def fix_authorized_keys_permissions(winrm_client, path):
147 147 commands = [
148 148 '$ErrorActionPreference = "Stop"',
149 149 'Repair-AuthorizedKeyPermission -FilePath %s -Confirm:$false' % path,
150 150 r'icacls %s /remove:g "NT Service\sshd"' % path,
151 151 ]
152 152
153 153 run_powershell(winrm_client, '\n'.join(commands))
154 154
155 155
156 156 def synchronize_hg(hg_repo: pathlib.Path, revision: str, ec2_instance):
157 157 """Synchronize local Mercurial repo to remote EC2 instance."""
158 158
159 159 winrm_client = ec2_instance.winrm_client
160 160
161 161 with tempfile.TemporaryDirectory() as temp_dir:
162 162 temp_dir = pathlib.Path(temp_dir)
163 163
164 164 ssh_dir = temp_dir / '.ssh'
165 165 ssh_dir.mkdir()
166 166 ssh_dir.chmod(0o0700)
167 167
168 168 # Generate SSH key to use for communication.
169 169 subprocess.run(
170 170 [
171 171 'ssh-keygen',
172 172 '-t',
173 173 'rsa',
174 174 '-b',
175 175 '4096',
176 176 '-N',
177 177 '',
178 178 '-f',
179 179 str(ssh_dir / 'id_rsa'),
180 180 ],
181 181 check=True,
182 182 capture_output=True,
183 183 )
184 184
185 185 # Add it to ~/.ssh/authorized_keys on remote.
186 186 # This assumes the file doesn't already exist.
187 187 authorized_keys = r'c:\Users\Administrator\.ssh\authorized_keys'
188 188 winrm_client.execute_cmd(r'mkdir c:\Users\Administrator\.ssh')
189 189 winrm_client.copy(str(ssh_dir / 'id_rsa.pub'), authorized_keys)
190 190 fix_authorized_keys_permissions(winrm_client, authorized_keys)
191 191
192 192 public_ip = ec2_instance.public_ip_address
193 193
194 194 ssh_config = temp_dir / '.ssh' / 'config'
195 195
196 196 with open(ssh_config, 'w', encoding='utf-8') as fh:
197 197 fh.write('Host %s\n' % public_ip)
198 198 fh.write(' User Administrator\n')
199 199 fh.write(' StrictHostKeyChecking no\n')
200 200 fh.write(' UserKnownHostsFile %s\n' % (ssh_dir / 'known_hosts'))
201 201 fh.write(' IdentityFile %s\n' % (ssh_dir / 'id_rsa'))
202 202
203 203 if not (hg_repo / '.hg').is_dir():
204 204 raise Exception(
205 205 '%s is not a Mercurial repository; '
206 206 'synchronization not yet supported' % hg_repo
207 207 )
208 208
209 209 env = dict(os.environ)
210 210 env['HGPLAIN'] = '1'
211 211 env['HGENCODING'] = 'utf-8'
212 212
213 213 hg_bin = hg_repo / 'hg'
214 214
215 215 res = subprocess.run(
216 216 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
217 217 cwd=str(hg_repo),
218 218 env=env,
219 219 check=True,
220 220 capture_output=True,
221 221 )
222 222
223 223 full_revision = res.stdout.decode('ascii')
224 224
225 225 args = [
226 226 'python2.7',
227 227 hg_bin,
228 228 '--config',
229 229 'ui.ssh=ssh -F %s' % ssh_config,
230 230 '--config',
231 231 'ui.remotecmd=c:/hgdev/venv-bootstrap/Scripts/hg.exe',
232 232 # Also ensure .hgtags changes are present so auto version
233 233 # calculation works.
234 234 'push',
235 235 '-f',
236 236 '-r',
237 237 full_revision,
238 238 '-r',
239 239 'file(.hgtags)',
240 240 'ssh://%s/c:/hgdev/src' % public_ip,
241 241 ]
242 242
243 243 res = subprocess.run(args, cwd=str(hg_repo), env=env)
244 244
245 245 # Allow 1 (no-op) to not trigger error.
246 246 if res.returncode not in (0, 1):
247 247 res.check_returncode()
248 248
249 249 run_powershell(
250 250 winrm_client, HG_UPDATE_CLEAN.format(revision=full_revision)
251 251 )
252 252
253 253 # TODO detect dirty local working directory and synchronize accordingly.
254 254
255 255
256 256 def purge_hg(winrm_client):
257 257 """Purge the Mercurial source repository on an EC2 instance."""
258 258 run_powershell(winrm_client, HG_PURGE)
259 259
260 260
261 261 def find_latest_dist(winrm_client, pattern):
262 262 """Find path to newest file in dist/ directory matching a pattern."""
263 263
264 264 res = winrm_client.execute_ps(
265 265 r'$v = Get-ChildItem -Path C:\hgdev\src\dist -Filter "%s" '
266 266 '| Sort-Object LastWriteTime -Descending '
267 267 '| Select-Object -First 1\n'
268 268 '$v.name' % pattern
269 269 )
270 270 return res[0]
271 271
272 272
273 273 def copy_latest_dist(winrm_client, pattern, dest_path):
274 274 """Copy latest file matching pattern in dist/ directory.
275 275
276 276 Given a WinRM client and a file pattern, find the latest file on the remote
277 277 matching that pattern and copy it to the ``dest_path`` directory on the
278 278 local machine.
279 279 """
280 280 latest = find_latest_dist(winrm_client, pattern)
281 281 source = r'C:\hgdev\src\dist\%s' % latest
282 282 dest = dest_path / latest
283 283 print('copying %s to %s' % (source, dest))
284 284 winrm_client.fetch(source, str(dest))
285 285
286 286
287 287 def build_inno_installer(
288 288 winrm_client, arch: str, dest_path: pathlib.Path, version=None
289 289 ):
290 290 """Build the Inno Setup installer on a remote machine.
291 291
292 292 Using a WinRM client, remote commands are executed to build
293 293 a Mercurial Inno Setup installer.
294 294 """
295 295 print('building Inno Setup installer for %s' % arch)
296 296
297 297 extra_args = []
298 298 if version:
299 299 extra_args.extend(['--version', version])
300 300
301 301 ps = get_vc_prefix(arch) + BUILD_INNO.format(
302 302 arch=arch, extra_args=' '.join(extra_args)
303 303 )
304 304 run_powershell(winrm_client, ps)
305 305 copy_latest_dist(winrm_client, '*.exe', dest_path)
306 306
307 307
308 308 def build_wheel(
309 309 winrm_client, python_version: str, arch: str, dest_path: pathlib.Path
310 310 ):
311 311 """Build Python wheels on a remote machine.
312 312
313 313 Using a WinRM client, remote commands are executed to build a Python wheel
314 314 for Mercurial.
315 315 """
316 316 print('Building Windows wheel for Python %s %s' % (python_version, arch))
317 317
318 318 ps = BUILD_WHEEL.format(
319 319 python_version=python_version.replace(".", ""), arch=arch
320 320 )
321 321
322 322 # Python 2.7 requires an activated environment.
323 323 if python_version == "2.7":
324 324 ps = get_vc_prefix(arch) + ps
325 325
326 326 run_powershell(winrm_client, ps)
327 327 copy_latest_dist(winrm_client, '*.whl', dest_path)
328 328
329 329
330 330 def build_wix_installer(
331 331 winrm_client, arch: str, dest_path: pathlib.Path, version=None
332 332 ):
333 333 """Build the WiX installer on a remote machine.
334 334
335 335 Using a WinRM client, remote commands are executed to build a WiX installer.
336 336 """
337 337 print('Building WiX installer for %s' % arch)
338 338 extra_args = []
339 339 if version:
340 340 extra_args.extend(['--version', version])
341 341
342 342 ps = get_vc_prefix(arch) + BUILD_WIX.format(
343 343 arch=arch, extra_args=' '.join(extra_args)
344 344 )
345 345 run_powershell(winrm_client, ps)
346 346 copy_latest_dist(winrm_client, '*.msi', dest_path)
347 347
348 348
349 349 def run_tests(winrm_client, python_version, arch, test_flags=''):
350 350 """Run tests on a remote Windows machine.
351 351
352 352 ``python_version`` is a ``X.Y`` string like ``2.7`` or ``3.7``.
353 353 ``arch`` is ``x86`` or ``x64``.
354 354 ``test_flags`` is a str representing extra arguments to pass to
355 355 ``run-tests.py``.
356 356 """
357 357 if not re.match(r'\d\.\d', python_version):
358 358 raise ValueError(
359 359 r'python_version must be \d.\d; got %s' % python_version
360 360 )
361 361
362 362 if arch not in ('x86', 'x64'):
363 363 raise ValueError('arch must be x86 or x64; got %s' % arch)
364 364
365 365 python_path = 'python%s-%s' % (python_version.replace('.', ''), arch)
366 366
367 367 ps = RUN_TESTS.format(python_path=python_path, test_flags=test_flags or '',)
368 368
369 369 run_powershell(winrm_client, ps)
370 370
371 371
372 372 def resolve_wheel_artifacts(dist_path: pathlib.Path, version: str):
373 373 return (
374 374 dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
375 375 dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
376 376 dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
377 377 dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
378 378 dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
379 379 dist_path / WHEEL_FILENAME_PYTHON38_X64.format(version=version),
380 380 )
381 381
382 382
383 383 def resolve_all_artifacts(dist_path: pathlib.Path, version: str):
384 384 return (
385 385 dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
386 386 dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
387 387 dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
388 388 dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
389 389 dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
390 390 dist_path / WHEEL_FILENAME_PYTHON38_X64.format(version=version),
391 391 dist_path / X86_EXE_FILENAME.format(version=version),
392 392 dist_path / X64_EXE_FILENAME.format(version=version),
393 393 dist_path / X86_MSI_FILENAME.format(version=version),
394 394 dist_path / X64_MSI_FILENAME.format(version=version),
395 395 )
396 396
397 397
398 398 def generate_latest_dat(version: str):
399 399 x86_exe_filename = X86_EXE_FILENAME.format(version=version)
400 400 x64_exe_filename = X64_EXE_FILENAME.format(version=version)
401 401 x86_msi_filename = X86_MSI_FILENAME.format(version=version)
402 402 x64_msi_filename = X64_MSI_FILENAME.format(version=version)
403 403
404 404 entries = (
405 405 (
406 406 '10',
407 407 version,
408 408 X86_USER_AGENT_PATTERN,
409 409 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x86_exe_filename),
410 410 X86_EXE_DESCRIPTION.format(version=version),
411 411 ),
412 412 (
413 413 '10',
414 414 version,
415 415 X64_USER_AGENT_PATTERN,
416 416 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_exe_filename),
417 417 X64_EXE_DESCRIPTION.format(version=version),
418 418 ),
419 419 (
420 420 '10',
421 421 version,
422 422 X86_USER_AGENT_PATTERN,
423 423 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x86_msi_filename),
424 424 X86_MSI_DESCRIPTION.format(version=version),
425 425 ),
426 426 (
427 427 '10',
428 428 version,
429 429 X64_USER_AGENT_PATTERN,
430 430 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_msi_filename),
431 431 X64_MSI_DESCRIPTION.format(version=version),
432 432 ),
433 433 )
434 434
435 435 lines = ['\t'.join(e) for e in entries]
436 436
437 437 return '\n'.join(lines) + '\n'
438 438
439 439
440 440 def publish_artifacts_pypi(dist_path: pathlib.Path, version: str):
441 441 """Publish Windows release artifacts to PyPI."""
442 442
443 443 wheel_paths = resolve_wheel_artifacts(dist_path, version)
444 444
445 445 for p in wheel_paths:
446 446 if not p.exists():
447 447 raise Exception('%s not found' % p)
448 448
449 449 print('uploading wheels to PyPI (you may be prompted for credentials)')
450 450 pypi_upload(wheel_paths)
451 451
452 452
453 453 def publish_artifacts_mercurial_scm_org(
454 454 dist_path: pathlib.Path, version: str, ssh_username=None
455 455 ):
456 456 """Publish Windows release artifacts to mercurial-scm.org."""
457 457 all_paths = resolve_all_artifacts(dist_path, version)
458 458
459 459 for p in all_paths:
460 460 if not p.exists():
461 461 raise Exception('%s not found' % p)
462 462
463 463 client = paramiko.SSHClient()
464 464 client.load_system_host_keys()
465 465 # We assume the system SSH configuration knows how to connect.
466 466 print('connecting to mercurial-scm.org via ssh...')
467 467 try:
468 468 client.connect('mercurial-scm.org', username=ssh_username)
469 469 except paramiko.AuthenticationException:
470 470 print('error authenticating; is an SSH key available in an SSH agent?')
471 471 raise
472 472
473 473 print('SSH connection established')
474 474
475 475 print('opening SFTP client...')
476 476 sftp = client.open_sftp()
477 477 print('SFTP client obtained')
478 478
479 479 for p in all_paths:
480 480 dest_path = '/var/www/release/windows/%s' % p.name
481 481 print('uploading %s to %s' % (p, dest_path))
482 482
483 483 with p.open('rb') as fh:
484 484 data = fh.read()
485 485
486 486 with sftp.open(dest_path, 'wb') as fh:
487 487 fh.write(data)
488 488 fh.chmod(0o0664)
489 489
490 490 latest_dat_path = '/var/www/release/windows/latest.dat'
491 491
492 492 now = datetime.datetime.utcnow()
493 493 backup_path = dist_path / (
494 494 'latest-windows-%s.dat' % now.strftime('%Y%m%dT%H%M%S')
495 495 )
496 496 print('backing up %s to %s' % (latest_dat_path, backup_path))
497 497
498 498 with sftp.open(latest_dat_path, 'rb') as fh:
499 499 latest_dat_old = fh.read()
500 500
501 501 with backup_path.open('wb') as fh:
502 502 fh.write(latest_dat_old)
503 503
504 504 print('writing %s with content:' % latest_dat_path)
505 505 latest_dat_content = generate_latest_dat(version)
506 506 print(latest_dat_content)
507 507
508 508 with sftp.open(latest_dat_path, 'wb') as fh:
509 509 fh.write(latest_dat_content.encode('ascii'))
510 510
511 511
512 512 def publish_artifacts(
513 513 dist_path: pathlib.Path,
514 514 version: str,
515 515 pypi=True,
516 516 mercurial_scm_org=True,
517 517 ssh_username=None,
518 518 ):
519 519 """Publish Windows release artifacts.
520 520
521 521 Files are found in `dist_path`. We will look for files with version string
522 522 `version`.
523 523
524 524 `pypi` controls whether we upload to PyPI.
525 525 `mercurial_scm_org` controls whether we upload to mercurial-scm.org.
526 526 """
527 527 if pypi:
528 528 publish_artifacts_pypi(dist_path, version)
529 529
530 530 if mercurial_scm_org:
531 531 publish_artifacts_mercurial_scm_org(
532 532 dist_path, version, ssh_username=ssh_username
533 533 )
@@ -1,227 +1,232 b''
1 1 # inno.py - Inno Setup 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 os
11 11 import pathlib
12 12 import shutil
13 13 import subprocess
14 14
15 15 import jinja2
16 16
17 17 from .py2exe import (
18 18 build_py2exe,
19 19 stage_install,
20 20 )
21 21 from .pyoxidizer import run_pyoxidizer
22 22 from .util import (
23 23 find_legacy_vc_runtime_files,
24 24 normalize_windows_version,
25 25 process_install_rules,
26 26 read_version_py,
27 27 )
28 28
29 29 EXTRA_PACKAGES = {
30 30 'dulwich',
31 31 'keyring',
32 32 'pygments',
33 33 'win32ctypes',
34 34 }
35 35
36 36 EXTRA_INSTALL_RULES = [
37 37 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
38 38 ]
39 39
40 40 PACKAGE_FILES_METADATA = {
41 41 'ReadMe.html': 'Flags: isreadme',
42 42 }
43 43
44 44
45 45 def build_with_py2exe(
46 46 source_dir: pathlib.Path,
47 47 build_dir: pathlib.Path,
48 48 python_exe: pathlib.Path,
49 49 iscc_exe: pathlib.Path,
50 50 version=None,
51 51 ):
52 52 """Build the Inno installer using py2exe.
53 53
54 54 Build files will be placed in ``build_dir``.
55 55
56 56 py2exe's setup.py doesn't use setuptools. It doesn't have modern logic
57 57 for finding the Python 2.7 toolchain. So, we require the environment
58 58 to already be configured with an active toolchain.
59 59 """
60 60 if not iscc_exe.exists():
61 61 raise Exception('%s does not exist' % iscc_exe)
62 62
63 63 vc_x64 = r'\x64' in os.environ.get('LIB', '')
64 64 arch = 'x64' if vc_x64 else 'x86'
65 65 inno_build_dir = build_dir / ('inno-py2exe-%s' % arch)
66 66 staging_dir = inno_build_dir / 'stage'
67 67
68 68 requirements_txt = (
69 69 source_dir / 'contrib' / 'packaging' / 'requirements_win32.txt'
70 70 )
71 71
72 72 inno_build_dir.mkdir(parents=True, exist_ok=True)
73 73
74 74 build_py2exe(
75 75 source_dir,
76 76 build_dir,
77 77 python_exe,
78 78 'inno',
79 79 requirements_txt,
80 80 extra_packages=EXTRA_PACKAGES,
81 81 )
82 82
83 83 # Purge the staging directory for every build so packaging is
84 84 # pristine.
85 85 if staging_dir.exists():
86 86 print('purging %s' % staging_dir)
87 87 shutil.rmtree(staging_dir)
88 88
89 89 # Now assemble all the packaged files into the staging directory.
90 90 stage_install(source_dir, staging_dir)
91 91
92 92 # We also install some extra files.
93 93 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
94 94
95 95 # hg.exe depends on VC9 runtime DLLs. Copy those into place.
96 96 for f in find_legacy_vc_runtime_files(vc_x64):
97 97 if f.name.endswith('.manifest'):
98 98 basename = 'Microsoft.VC90.CRT.manifest'
99 99 else:
100 100 basename = f.name
101 101
102 102 dest_path = staging_dir / basename
103 103
104 104 print('copying %s to %s' % (f, dest_path))
105 105 shutil.copyfile(f, dest_path)
106 106
107 107 build_installer(
108 108 source_dir,
109 109 inno_build_dir,
110 110 staging_dir,
111 111 iscc_exe,
112 112 version,
113 113 arch="x64" if vc_x64 else None,
114 suffix="-python2",
114 115 )
115 116
116 117
117 118 def build_with_pyoxidizer(
118 119 source_dir: pathlib.Path,
119 120 build_dir: pathlib.Path,
120 121 target_triple: str,
121 122 iscc_exe: pathlib.Path,
122 123 version=None,
123 124 ):
124 125 """Build the Inno installer using PyOxidizer."""
125 126 if not iscc_exe.exists():
126 127 raise Exception("%s does not exist" % iscc_exe)
127 128
128 129 inno_build_dir = build_dir / ("inno-pyoxidizer-%s" % target_triple)
129 130 staging_dir = inno_build_dir / "stage"
130 131
131 132 inno_build_dir.mkdir(parents=True, exist_ok=True)
132 133 run_pyoxidizer(source_dir, inno_build_dir, staging_dir, target_triple)
133 134
134 135 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
135 136
136 137 build_installer(
137 138 source_dir,
138 139 inno_build_dir,
139 140 staging_dir,
140 141 iscc_exe,
141 142 version,
142 143 arch="x64" if "x86_64" in target_triple else None,
143 144 )
144 145
145 146
146 147 def build_installer(
147 148 source_dir: pathlib.Path,
148 149 inno_build_dir: pathlib.Path,
149 150 staging_dir: pathlib.Path,
150 151 iscc_exe: pathlib.Path,
151 152 version,
152 153 arch=None,
154 suffix="",
153 155 ):
154 156 """Build an Inno installer from staged Mercurial files.
155 157
156 158 This function is agnostic about how to build Mercurial. It just
157 159 cares that Mercurial files are in ``staging_dir``.
158 160 """
159 161 inno_source_dir = source_dir / "contrib" / "packaging" / "inno"
160 162
161 163 # The final package layout is simply a mirror of the staging directory.
162 164 package_files = []
163 165 for root, dirs, files in os.walk(staging_dir):
164 166 dirs.sort()
165 167
166 168 root = pathlib.Path(root)
167 169
168 170 for f in sorted(files):
169 171 full = root / f
170 172 rel = full.relative_to(staging_dir)
171 173 if str(rel.parent) == '.':
172 174 dest_dir = '{app}'
173 175 else:
174 176 dest_dir = '{app}\\%s' % rel.parent
175 177
176 178 package_files.append(
177 179 {
178 180 'source': rel,
179 181 'dest_dir': dest_dir,
180 182 'metadata': PACKAGE_FILES_METADATA.get(str(rel), None),
181 183 }
182 184 )
183 185
184 186 print('creating installer')
185 187
186 188 # Install Inno files by rendering a template.
187 189 jinja_env = jinja2.Environment(
188 190 loader=jinja2.FileSystemLoader(str(inno_source_dir)),
189 191 # Need to change these to prevent conflict with Inno Setup.
190 192 comment_start_string='{##',
191 193 comment_end_string='##}',
192 194 )
193 195
194 196 try:
195 197 template = jinja_env.get_template('mercurial.iss')
196 198 except jinja2.TemplateSyntaxError as e:
197 199 raise Exception(
198 200 'template syntax error at %s:%d: %s'
199 201 % (e.name, e.lineno, e.message,)
200 202 )
201 203
202 204 content = template.render(package_files=package_files)
203 205
204 206 with (inno_build_dir / 'mercurial.iss').open('w', encoding='utf-8') as fh:
205 207 fh.write(content)
206 208
207 209 # Copy additional files used by Inno.
208 210 for p in ('mercurial.ico', 'postinstall.txt'):
209 211 shutil.copyfile(
210 212 source_dir / 'contrib' / 'win32' / p, inno_build_dir / p
211 213 )
212 214
213 215 args = [str(iscc_exe)]
214 216
215 217 if arch:
216 218 args.append('/dARCH=%s' % arch)
219 args.append('/dSUFFIX=-%s%s' % (arch, suffix))
220 else:
221 args.append('/dSUFFIX=-x86%s' % suffix)
217 222
218 223 if not version:
219 224 version = read_version_py(source_dir)
220 225
221 226 args.append('/dVERSION=%s' % version)
222 227 args.append('/dQUAD_VERSION=%s' % normalize_windows_version(version))
223 228
224 229 args.append('/Odist')
225 230 args.append(str(inno_build_dir / 'mercurial.iss'))
226 231
227 232 subprocess.run(args, cwd=str(source_dir), check=True)
@@ -1,531 +1,535 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 .pyoxidizer import run_pyoxidizer
26 26 from .util import (
27 27 extract_zip_to_directory,
28 28 normalize_windows_version,
29 29 process_install_rules,
30 30 sign_with_signtool,
31 31 )
32 32
33 33
34 34 EXTRA_PACKAGES = {
35 35 'dulwich',
36 36 'distutils',
37 37 'keyring',
38 38 'pygments',
39 39 'win32ctypes',
40 40 }
41 41
42 42
43 43 EXTRA_INSTALL_RULES = [
44 44 ('contrib/packaging/wix/COPYING.rtf', 'COPYING.rtf'),
45 45 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
46 46 ]
47 47
48 48 STAGING_REMOVE_FILES = [
49 49 # We use the RTF variant.
50 50 'copying.txt',
51 51 ]
52 52
53 53 SHORTCUTS = {
54 54 # hg.1.html'
55 55 'hg.file.5d3e441c_28d9_5542_afd0_cdd4234f12d5': {
56 56 'Name': 'Mercurial Command Reference',
57 57 },
58 58 # hgignore.5.html
59 59 'hg.file.5757d8e0_f207_5e10_a2ec_3ba0a062f431': {
60 60 'Name': 'Mercurial Ignore Files',
61 61 },
62 62 # hgrc.5.html
63 63 'hg.file.92e605fd_1d1a_5dc6_9fc0_5d2998eb8f5e': {
64 64 'Name': 'Mercurial Configuration Files',
65 65 },
66 66 }
67 67
68 68
69 69 def find_version(source_dir: pathlib.Path):
70 70 version_py = source_dir / 'mercurial' / '__version__.py'
71 71
72 72 with version_py.open('r', encoding='utf-8') as fh:
73 73 source = fh.read().strip()
74 74
75 75 m = re.search('version = b"(.*)"', source)
76 76 return m.group(1)
77 77
78 78
79 79 def ensure_vc90_merge_modules(build_dir):
80 80 x86 = (
81 81 download_entry(
82 82 'vc9-crt-x86-msm',
83 83 build_dir,
84 84 local_name='microsoft.vcxx.crt.x86_msm.msm',
85 85 )[0],
86 86 download_entry(
87 87 'vc9-crt-x86-msm-policy',
88 88 build_dir,
89 89 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm',
90 90 )[0],
91 91 )
92 92
93 93 x64 = (
94 94 download_entry(
95 95 'vc9-crt-x64-msm',
96 96 build_dir,
97 97 local_name='microsoft.vcxx.crt.x64_msm.msm',
98 98 )[0],
99 99 download_entry(
100 100 'vc9-crt-x64-msm-policy',
101 101 build_dir,
102 102 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm',
103 103 )[0],
104 104 )
105 105 return {
106 106 'x86': x86,
107 107 'x64': x64,
108 108 }
109 109
110 110
111 111 def run_candle(wix, cwd, wxs, source_dir, defines=None):
112 112 args = [
113 113 str(wix / 'candle.exe'),
114 114 '-nologo',
115 115 str(wxs),
116 116 '-dSourceDir=%s' % source_dir,
117 117 ]
118 118
119 119 if defines:
120 120 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
121 121
122 122 subprocess.run(args, cwd=str(cwd), check=True)
123 123
124 124
125 125 def make_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
126 126 """Create XML string listing every file to be installed."""
127 127
128 128 # We derive GUIDs from a deterministic file path identifier.
129 129 # We shoehorn the name into something that looks like a URL because
130 130 # the UUID namespaces are supposed to work that way (even though
131 131 # the input data probably is never validated).
132 132
133 133 doc = xml.dom.minidom.parseString(
134 134 '<?xml version="1.0" encoding="utf-8"?>'
135 135 '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">'
136 136 '</Wix>'
137 137 )
138 138
139 139 # Assemble the install layout by directory. This makes it easier to
140 140 # emit XML, since each directory has separate entities.
141 141 manifest = collections.defaultdict(dict)
142 142
143 143 for root, dirs, files in os.walk(staging_dir):
144 144 dirs.sort()
145 145
146 146 root = pathlib.Path(root)
147 147 rel_dir = root.relative_to(staging_dir)
148 148
149 149 for i in range(len(rel_dir.parts)):
150 150 parent = '/'.join(rel_dir.parts[0 : i + 1])
151 151 manifest.setdefault(parent, {})
152 152
153 153 for f in sorted(files):
154 154 full = root / f
155 155 manifest[str(rel_dir).replace('\\', '/')][full.name] = full
156 156
157 157 component_groups = collections.defaultdict(list)
158 158
159 159 # Now emit a <Fragment> for each directory.
160 160 # Each directory is composed of a <DirectoryRef> pointing to its parent
161 161 # and defines child <Directory>'s and a <Component> with all the files.
162 162 for dir_name, entries in sorted(manifest.items()):
163 163 # The directory id is derived from the path. But the root directory
164 164 # is special.
165 165 if dir_name == '.':
166 166 parent_directory_id = 'INSTALLDIR'
167 167 else:
168 168 parent_directory_id = 'hg.dir.%s' % dir_name.replace('/', '.')
169 169
170 170 fragment = doc.createElement('Fragment')
171 171 directory_ref = doc.createElement('DirectoryRef')
172 172 directory_ref.setAttribute('Id', parent_directory_id)
173 173
174 174 # Add <Directory> entries for immediate children directories.
175 175 for possible_child in sorted(manifest.keys()):
176 176 if (
177 177 dir_name == '.'
178 178 and '/' not in possible_child
179 179 and possible_child != '.'
180 180 ):
181 181 child_directory_id = 'hg.dir.%s' % possible_child
182 182 name = possible_child
183 183 else:
184 184 if not possible_child.startswith('%s/' % dir_name):
185 185 continue
186 186 name = possible_child[len(dir_name) + 1 :]
187 187 if '/' in name:
188 188 continue
189 189
190 190 child_directory_id = 'hg.dir.%s' % possible_child.replace(
191 191 '/', '.'
192 192 )
193 193
194 194 directory = doc.createElement('Directory')
195 195 directory.setAttribute('Id', child_directory_id)
196 196 directory.setAttribute('Name', name)
197 197 directory_ref.appendChild(directory)
198 198
199 199 # Add <Component>s for files in this directory.
200 200 for rel, source_path in sorted(entries.items()):
201 201 if dir_name == '.':
202 202 full_rel = rel
203 203 else:
204 204 full_rel = '%s/%s' % (dir_name, rel)
205 205
206 206 component_unique_id = (
207 207 'https://www.mercurial-scm.org/wix-installer/0/component/%s'
208 208 % full_rel
209 209 )
210 210 component_guid = uuid.uuid5(uuid.NAMESPACE_URL, component_unique_id)
211 211 component_id = 'hg.component.%s' % str(component_guid).replace(
212 212 '-', '_'
213 213 )
214 214
215 215 component = doc.createElement('Component')
216 216
217 217 component.setAttribute('Id', component_id)
218 218 component.setAttribute('Guid', str(component_guid).upper())
219 219 component.setAttribute('Win64', 'yes' if is_x64 else 'no')
220 220
221 221 # Assign this component to a top-level group.
222 222 if dir_name == '.':
223 223 component_groups['ROOT'].append(component_id)
224 224 elif '/' in dir_name:
225 225 component_groups[dir_name[0 : dir_name.index('/')]].append(
226 226 component_id
227 227 )
228 228 else:
229 229 component_groups[dir_name].append(component_id)
230 230
231 231 unique_id = (
232 232 'https://www.mercurial-scm.org/wix-installer/0/%s' % full_rel
233 233 )
234 234 file_guid = uuid.uuid5(uuid.NAMESPACE_URL, unique_id)
235 235
236 236 # IDs have length limits. So use GUID to derive them.
237 237 file_guid_normalized = str(file_guid).replace('-', '_')
238 238 file_id = 'hg.file.%s' % file_guid_normalized
239 239
240 240 file_element = doc.createElement('File')
241 241 file_element.setAttribute('Id', file_id)
242 242 file_element.setAttribute('Source', str(source_path))
243 243 file_element.setAttribute('KeyPath', 'yes')
244 244 file_element.setAttribute('ReadOnly', 'yes')
245 245
246 246 component.appendChild(file_element)
247 247 directory_ref.appendChild(component)
248 248
249 249 fragment.appendChild(directory_ref)
250 250 doc.documentElement.appendChild(fragment)
251 251
252 252 for group, component_ids in sorted(component_groups.items()):
253 253 fragment = doc.createElement('Fragment')
254 254 component_group = doc.createElement('ComponentGroup')
255 255 component_group.setAttribute('Id', 'hg.group.%s' % group)
256 256
257 257 for component_id in component_ids:
258 258 component_ref = doc.createElement('ComponentRef')
259 259 component_ref.setAttribute('Id', component_id)
260 260 component_group.appendChild(component_ref)
261 261
262 262 fragment.appendChild(component_group)
263 263 doc.documentElement.appendChild(fragment)
264 264
265 265 # Add <Shortcut> to files that have it defined.
266 266 for file_id, metadata in sorted(SHORTCUTS.items()):
267 267 els = doc.getElementsByTagName('File')
268 268 els = [el for el in els if el.getAttribute('Id') == file_id]
269 269
270 270 if not els:
271 271 raise Exception('could not find File[Id=%s]' % file_id)
272 272
273 273 for el in els:
274 274 shortcut = doc.createElement('Shortcut')
275 275 shortcut.setAttribute('Id', 'hg.shortcut.%s' % file_id)
276 276 shortcut.setAttribute('Directory', 'ProgramMenuDir')
277 277 shortcut.setAttribute('Icon', 'hgIcon.ico')
278 278 shortcut.setAttribute('IconIndex', '0')
279 279 shortcut.setAttribute('Advertise', 'yes')
280 280 for k, v in sorted(metadata.items()):
281 281 shortcut.setAttribute(k, v)
282 282
283 283 el.appendChild(shortcut)
284 284
285 285 return doc.toprettyxml()
286 286
287 287
288 288 def build_installer_py2exe(
289 289 source_dir: pathlib.Path,
290 290 python_exe: pathlib.Path,
291 291 msi_name='mercurial',
292 292 version=None,
293 293 extra_packages_script=None,
294 294 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
295 295 extra_features: typing.Optional[typing.List[str]] = None,
296 296 signing_info: typing.Optional[typing.Dict[str, str]] = None,
297 297 ):
298 298 """Build a WiX MSI installer using py2exe.
299 299
300 300 ``source_dir`` is the path to the Mercurial source tree to use.
301 301 ``arch`` is the target architecture. either ``x86`` or ``x64``.
302 302 ``python_exe`` is the path to the Python executable to use/bundle.
303 303 ``version`` is the Mercurial version string. If not defined,
304 304 ``mercurial/__version__.py`` will be consulted.
305 305 ``extra_packages_script`` is a command to be run to inject extra packages
306 306 into the py2exe binary. It should stage packages into the virtualenv and
307 307 print a null byte followed by a newline-separated list of packages that
308 308 should be included in the exe.
309 309 ``extra_wxs`` is a dict of {wxs_name: working_dir_for_wxs_build}.
310 310 ``extra_features`` is a list of additional named Features to include in
311 311 the build. These must match Feature names in one of the wxs scripts.
312 312 """
313 313 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
314 314
315 315 hg_build_dir = source_dir / 'build'
316 316
317 317 requirements_txt = (
318 318 source_dir / 'contrib' / 'packaging' / 'requirements_win32.txt'
319 319 )
320 320
321 321 build_py2exe(
322 322 source_dir,
323 323 hg_build_dir,
324 324 python_exe,
325 325 'wix',
326 326 requirements_txt,
327 327 extra_packages=EXTRA_PACKAGES,
328 328 extra_packages_script=extra_packages_script,
329 329 )
330 330
331 331 build_dir = hg_build_dir / ('wix-%s' % arch)
332 332 staging_dir = build_dir / 'stage'
333 333
334 334 build_dir.mkdir(exist_ok=True)
335 335
336 336 # Purge the staging directory for every build so packaging is pristine.
337 337 if staging_dir.exists():
338 338 print('purging %s' % staging_dir)
339 339 shutil.rmtree(staging_dir)
340 340
341 341 stage_install(source_dir, staging_dir, lower_case=True)
342 342
343 343 # We also install some extra files.
344 344 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
345 345
346 346 # And remove some files we don't want.
347 347 for f in STAGING_REMOVE_FILES:
348 348 p = staging_dir / f
349 349 if p.exists():
350 350 print('removing %s' % p)
351 351 p.unlink()
352 352
353 353 return run_wix_packaging(
354 354 source_dir,
355 355 build_dir,
356 356 staging_dir,
357 357 arch,
358 358 version=version,
359 359 python2=True,
360 360 msi_name=msi_name,
361 suffix="-python2",
361 362 extra_wxs=extra_wxs,
362 363 extra_features=extra_features,
363 364 signing_info=signing_info,
364 365 )
365 366
366 367
367 368 def build_installer_pyoxidizer(
368 369 source_dir: pathlib.Path,
369 370 target_triple: str,
370 371 msi_name='mercurial',
371 372 version=None,
372 373 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
373 374 extra_features: typing.Optional[typing.List[str]] = None,
374 375 signing_info: typing.Optional[typing.Dict[str, str]] = None,
375 376 ):
376 377 """Build a WiX MSI installer using PyOxidizer."""
377 378 hg_build_dir = source_dir / "build"
378 379 build_dir = hg_build_dir / ("wix-%s" % target_triple)
379 380 staging_dir = build_dir / "stage"
380 381
381 382 arch = "x64" if "x86_64" in target_triple else "x86"
382 383
383 384 build_dir.mkdir(parents=True, exist_ok=True)
384 385 run_pyoxidizer(source_dir, build_dir, staging_dir, target_triple)
385 386
386 387 # We also install some extra files.
387 388 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
388 389
389 390 # And remove some files we don't want.
390 391 for f in STAGING_REMOVE_FILES:
391 392 p = staging_dir / f
392 393 if p.exists():
393 394 print('removing %s' % p)
394 395 p.unlink()
395 396
396 397 return run_wix_packaging(
397 398 source_dir,
398 399 build_dir,
399 400 staging_dir,
400 401 arch,
401 402 version,
402 403 python2=False,
403 404 msi_name=msi_name,
404 405 extra_wxs=extra_wxs,
405 406 extra_features=extra_features,
406 407 signing_info=signing_info,
407 408 )
408 409
409 410
410 411 def run_wix_packaging(
411 412 source_dir: pathlib.Path,
412 413 build_dir: pathlib.Path,
413 414 staging_dir: pathlib.Path,
414 415 arch: str,
415 416 version: str,
416 417 python2: bool,
417 418 msi_name: typing.Optional[str] = "mercurial",
419 suffix: str = "",
418 420 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
419 421 extra_features: typing.Optional[typing.List[str]] = None,
420 422 signing_info: typing.Optional[typing.Dict[str, str]] = None,
421 423 ):
422 424 """Invokes WiX to package up a built Mercurial.
423 425
424 426 ``signing_info`` is a dict defining properties to facilitate signing the
425 427 installer. Recognized keys include ``name``, ``subject_name``,
426 428 ``cert_path``, ``cert_password``, and ``timestamp_url``. If populated,
427 429 we will sign both the hg.exe and the .msi using the signing credentials
428 430 specified.
429 431 """
430 432
431 433 orig_version = version or find_version(source_dir)
432 434 version = normalize_windows_version(orig_version)
433 435 print('using version string: %s' % version)
434 436 if version != orig_version:
435 437 print('(normalized from: %s)' % orig_version)
436 438
437 439 if signing_info:
438 440 sign_with_signtool(
439 441 staging_dir / "hg.exe",
440 442 "%s %s" % (signing_info["name"], version),
441 443 subject_name=signing_info["subject_name"],
442 444 cert_path=signing_info["cert_path"],
443 445 cert_password=signing_info["cert_password"],
444 446 timestamp_url=signing_info["timestamp_url"],
445 447 )
446 448
447 449 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
448 450
449 451 wix_pkg, wix_entry = download_entry('wix', build_dir)
450 452 wix_path = build_dir / ('wix-%s' % wix_entry['version'])
451 453
452 454 if not wix_path.exists():
453 455 extract_zip_to_directory(wix_pkg, wix_path)
454 456
455 457 if python2:
456 458 ensure_vc90_merge_modules(build_dir)
457 459
458 460 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
459 461
460 462 defines = {'Platform': arch}
461 463
462 464 # Derive a .wxs file with the staged files.
463 465 manifest_wxs = build_dir / 'stage.wxs'
464 466 with manifest_wxs.open('w', encoding='utf-8') as fh:
465 467 fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
466 468
467 469 run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
468 470
469 471 for source, rel_path in sorted((extra_wxs or {}).items()):
470 472 run_candle(wix_path, build_dir, source, rel_path, defines=defines)
471 473
472 474 source = wix_dir / 'mercurial.wxs'
473 475 defines['Version'] = version
474 476 defines['Comments'] = 'Installs Mercurial version %s' % version
475 477
476 478 if python2:
477 479 defines["PythonVersion"] = "2"
478 480 defines['VCRedistSrcDir'] = str(build_dir)
479 481 else:
480 482 defines["PythonVersion"] = "3"
481 483
482 484 if (staging_dir / "lib").exists():
483 485 defines["MercurialHasLib"] = "1"
484 486
485 487 if extra_features:
486 488 assert all(';' not in f for f in extra_features)
487 489 defines['MercurialExtraFeatures'] = ';'.join(extra_features)
488 490
489 491 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
490 492
491 493 msi_path = (
492 source_dir / 'dist' / ('%s-%s-%s.msi' % (msi_name, orig_version, arch))
494 source_dir
495 / 'dist'
496 / ('%s-%s-%s%s.msi' % (msi_name, orig_version, arch, suffix))
493 497 )
494 498
495 499 args = [
496 500 str(wix_path / 'light.exe'),
497 501 '-nologo',
498 502 '-ext',
499 503 'WixUIExtension',
500 504 '-sw1076',
501 505 '-spdb',
502 506 '-o',
503 507 str(msi_path),
504 508 ]
505 509
506 510 for source, rel_path in sorted((extra_wxs or {}).items()):
507 511 assert source.endswith('.wxs')
508 512 source = os.path.basename(source)
509 513 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
510 514
511 515 args.extend(
512 516 [str(build_dir / 'stage.wixobj'), str(build_dir / 'mercurial.wixobj'),]
513 517 )
514 518
515 519 subprocess.run(args, cwd=str(source_dir), check=True)
516 520
517 521 print('%s created' % msi_path)
518 522
519 523 if signing_info:
520 524 sign_with_signtool(
521 525 msi_path,
522 526 "%s %s" % (signing_info["name"], version),
523 527 subject_name=signing_info["subject_name"],
524 528 cert_path=signing_info["cert_path"],
525 529 cert_password=signing_info["cert_password"],
526 530 timestamp_url=signing_info["timestamp_url"],
527 531 )
528 532
529 533 return {
530 534 'msi_path': msi_path,
531 535 }
@@ -1,83 +1,82 b''
1 1 ; Script generated by the Inno Setup Script Wizard.
2 2 ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
3 3
4 4 #ifndef ARCH
5 5 #define ARCH = "x86"
6 6 #endif
7 7
8 8 [Setup]
9 9 AppCopyright=Copyright 2005-2020 Matt Mackall and others
10 10 AppName=Mercurial
11 11 AppVersion={#VERSION}
12 OutputBaseFilename=Mercurial-{#VERSION}{#SUFFIX}
12 13 #if ARCH == "x64"
13 14 AppVerName=Mercurial {#VERSION} (64-bit)
14 OutputBaseFilename=Mercurial-{#VERSION}-x64
15 15 ArchitecturesAllowed=x64
16 16 ArchitecturesInstallIn64BitMode=x64
17 17 #else
18 18 AppVerName=Mercurial {#VERSION}
19 OutputBaseFilename=Mercurial-{#VERSION}
20 19 #endif
21 20 InfoAfterFile=../postinstall.txt
22 21 LicenseFile=Copying.txt
23 22 ShowLanguageDialog=yes
24 23 AppPublisher=Matt Mackall and others
25 24 AppPublisherURL=https://mercurial-scm.org/
26 25 AppSupportURL=https://mercurial-scm.org/
27 26 AppUpdatesURL=https://mercurial-scm.org/
28 27 {{ 'AppID={{4B95A5F1-EF59-4B08-BED8-C891C46121B3}' }}
29 28 AppContact=mercurial@mercurial-scm.org
30 29 DefaultDirName={pf}\Mercurial
31 30 SourceDir=stage
32 31 VersionInfoDescription=Mercurial distributed SCM (version {#VERSION})
33 32 VersionInfoCopyright=Copyright 2005-2020 Matt Mackall and others
34 33 VersionInfoCompany=Matt Mackall and others
35 34 VersionInfoVersion={#QUAD_VERSION}
36 35 InternalCompressLevel=max
37 36 SolidCompression=true
38 37 SetupIconFile=../mercurial.ico
39 38 AllowNoIcons=true
40 39 DefaultGroupName=Mercurial
41 40 PrivilegesRequired=none
42 41 ChangesEnvironment=true
43 42
44 43 [Files]
45 44 {% for entry in package_files -%}
46 45 Source: {{ entry.source }}; DestDir: {{ entry.dest_dir }}
47 46 {%- if entry.metadata %}; {{ entry.metadata }}{% endif %}
48 47 {% endfor %}
49 48
50 49 [INI]
51 50 Filename: {app}\Mercurial.url; Section: InternetShortcut; Key: URL; String: https://mercurial-scm.org/
52 51
53 52 [UninstallDelete]
54 53 Type: files; Name: {app}\Mercurial.url
55 54 Type: filesandordirs; Name: {app}\defaultrc
56 55
57 56 [Icons]
58 57 Name: {group}\Uninstall Mercurial; Filename: {uninstallexe}
59 58 Name: {group}\Mercurial Command Reference; Filename: {app}\Docs\hg.1.html
60 59 Name: {group}\Mercurial Configuration Files; Filename: {app}\Docs\hgrc.5.html
61 60 Name: {group}\Mercurial Ignore Files; Filename: {app}\Docs\hgignore.5.html
62 61 Name: {group}\Mercurial Web Site; Filename: {app}\Mercurial.url
63 62
64 63 [Tasks]
65 64 Name: modifypath; Description: Add the installation path to the search path; Flags: unchecked
66 65
67 66 [Code]
68 67 procedure Touch(fn: String);
69 68 begin
70 69 SaveStringToFile(ExpandConstant(fn), '', False);
71 70 end;
72 71
73 72 const
74 73 ModPathName = 'modifypath';
75 74 ModPathType = 'user';
76 75
77 76 function ModPathDir(): TArrayOfString;
78 77 begin
79 78 setArrayLength(Result, 1)
80 79 Result[0] := ExpandConstant('{app}');
81 80 end;
82 81
83 82 {% include 'modpath.iss' %}
General Comments 0
You need to be logged in to leave comments. Login now