##// END OF EJS Templates
automation: support building Python 3 MSI installers...
Gregory Szorc -
r45279:5e788dc7 stable
parent child Browse files
Show More
@@ -1,532 +1,548 b''
1 1 # cli.py - Command line interface for automation
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 argparse
11 11 import concurrent.futures as futures
12 12 import os
13 13 import pathlib
14 14 import time
15 15
16 16 from . import (
17 17 aws,
18 18 HGAutomation,
19 19 linux,
20 20 try_server,
21 21 windows,
22 22 )
23 23
24 24
25 25 SOURCE_ROOT = pathlib.Path(
26 26 os.path.abspath(__file__)
27 27 ).parent.parent.parent.parent
28 28 DIST_PATH = SOURCE_ROOT / 'dist'
29 29
30 30
31 31 def bootstrap_linux_dev(
32 32 hga: HGAutomation, aws_region, distros=None, parallel=False
33 33 ):
34 34 c = hga.aws_connection(aws_region)
35 35
36 36 if distros:
37 37 distros = distros.split(',')
38 38 else:
39 39 distros = sorted(linux.DISTROS)
40 40
41 41 # TODO There is a wonky interaction involving KeyboardInterrupt whereby
42 42 # the context manager that is supposed to terminate the temporary EC2
43 43 # instance doesn't run. Until we fix this, make parallel building opt-in
44 44 # so we don't orphan instances.
45 45 if parallel:
46 46 fs = []
47 47
48 48 with futures.ThreadPoolExecutor(len(distros)) as e:
49 49 for distro in distros:
50 50 fs.append(e.submit(aws.ensure_linux_dev_ami, c, distro=distro))
51 51
52 52 for f in fs:
53 53 f.result()
54 54 else:
55 55 for distro in distros:
56 56 aws.ensure_linux_dev_ami(c, distro=distro)
57 57
58 58
59 59 def bootstrap_windows_dev(hga: HGAutomation, aws_region, base_image_name):
60 60 c = hga.aws_connection(aws_region)
61 61 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
62 62 print('Windows development AMI available as %s' % image.id)
63 63
64 64
65 65 def build_inno(
66 66 hga: HGAutomation,
67 67 aws_region,
68 68 python_version,
69 69 arch,
70 70 revision,
71 71 version,
72 72 base_image_name,
73 73 ):
74 74 c = hga.aws_connection(aws_region)
75 75 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
76 76 DIST_PATH.mkdir(exist_ok=True)
77 77
78 78 with aws.temporary_windows_dev_instances(c, image, 't3.medium') as insts:
79 79 instance = insts[0]
80 80
81 81 windows.synchronize_hg(SOURCE_ROOT, revision, instance)
82 82
83 83 for py_version in python_version:
84 84 for a in arch:
85 85 windows.build_inno_installer(
86 86 instance.winrm_client,
87 87 py_version,
88 88 a,
89 89 DIST_PATH,
90 90 version=version,
91 91 )
92 92
93 93
94 94 def build_wix(
95 hga: HGAutomation, aws_region, arch, revision, version, base_image_name
95 hga: HGAutomation,
96 aws_region,
97 python_version,
98 arch,
99 revision,
100 version,
101 base_image_name,
96 102 ):
97 103 c = hga.aws_connection(aws_region)
98 104 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
99 105 DIST_PATH.mkdir(exist_ok=True)
100 106
101 107 with aws.temporary_windows_dev_instances(c, image, 't3.medium') as insts:
102 108 instance = insts[0]
103 109
104 110 windows.synchronize_hg(SOURCE_ROOT, revision, instance)
105 111
106 for a in arch:
107 windows.build_wix_installer(
108 instance.winrm_client, a, DIST_PATH, version=version
109 )
112 for py_version in python_version:
113 for a in arch:
114 windows.build_wix_installer(
115 instance.winrm_client,
116 py_version,
117 a,
118 DIST_PATH,
119 version=version,
120 )
110 121
111 122
112 123 def build_windows_wheel(
113 124 hga: HGAutomation,
114 125 aws_region,
115 126 python_version,
116 127 arch,
117 128 revision,
118 129 base_image_name,
119 130 ):
120 131 c = hga.aws_connection(aws_region)
121 132 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
122 133 DIST_PATH.mkdir(exist_ok=True)
123 134
124 135 with aws.temporary_windows_dev_instances(c, image, 't3.medium') as insts:
125 136 instance = insts[0]
126 137
127 138 windows.synchronize_hg(SOURCE_ROOT, revision, instance)
128 139
129 140 for py_version in python_version:
130 141 for a in arch:
131 142 windows.build_wheel(
132 143 instance.winrm_client, py_version, a, DIST_PATH
133 144 )
134 145
135 146
136 147 def build_all_windows_packages(
137 148 hga: HGAutomation, aws_region, revision, version, base_image_name
138 149 ):
139 150 c = hga.aws_connection(aws_region)
140 151 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
141 152 DIST_PATH.mkdir(exist_ok=True)
142 153
143 154 with aws.temporary_windows_dev_instances(c, image, 't3.medium') as insts:
144 155 instance = insts[0]
145 156
146 157 winrm_client = instance.winrm_client
147 158
148 159 windows.synchronize_hg(SOURCE_ROOT, revision, instance)
149 160
150 161 for py_version in ("2.7", "3.7", "3.8"):
151 162 for arch in ("x86", "x64"):
152 163 windows.purge_hg(winrm_client)
153 164 windows.build_wheel(
154 165 winrm_client,
155 166 python_version=py_version,
156 167 arch=arch,
157 168 dest_path=DIST_PATH,
158 169 )
159 170
160 171 for py_version in (2, 3):
161 172 for arch in ('x86', 'x64'):
162 173 windows.purge_hg(winrm_client)
163 174 windows.build_inno_installer(
164 175 winrm_client, py_version, arch, DIST_PATH, version=version
165 176 )
166
167 for arch in ('x86', 'x64'):
168 windows.purge_hg(winrm_client)
169 windows.build_wix_installer(
170 winrm_client, arch, DIST_PATH, version=version
171 )
177 windows.build_wix_installer(
178 winrm_client, py_version, arch, DIST_PATH, version=version
179 )
172 180
173 181
174 182 def terminate_ec2_instances(hga: HGAutomation, aws_region):
175 183 c = hga.aws_connection(aws_region, ensure_ec2_state=False)
176 184 aws.terminate_ec2_instances(c.ec2resource)
177 185
178 186
179 187 def purge_ec2_resources(hga: HGAutomation, aws_region):
180 188 c = hga.aws_connection(aws_region, ensure_ec2_state=False)
181 189 aws.remove_resources(c)
182 190
183 191
184 192 def run_tests_linux(
185 193 hga: HGAutomation,
186 194 aws_region,
187 195 instance_type,
188 196 python_version,
189 197 test_flags,
190 198 distro,
191 199 filesystem,
192 200 ):
193 201 c = hga.aws_connection(aws_region)
194 202 image = aws.ensure_linux_dev_ami(c, distro=distro)
195 203
196 204 t_start = time.time()
197 205
198 206 ensure_extra_volume = filesystem not in ('default', 'tmpfs')
199 207
200 208 with aws.temporary_linux_dev_instances(
201 209 c, image, instance_type, ensure_extra_volume=ensure_extra_volume
202 210 ) as insts:
203 211
204 212 instance = insts[0]
205 213
206 214 linux.prepare_exec_environment(
207 215 instance.ssh_client, filesystem=filesystem
208 216 )
209 217 linux.synchronize_hg(SOURCE_ROOT, instance, '.')
210 218 t_prepared = time.time()
211 219 linux.run_tests(instance.ssh_client, python_version, test_flags)
212 220 t_done = time.time()
213 221
214 222 t_setup = t_prepared - t_start
215 223 t_all = t_done - t_start
216 224
217 225 print(
218 226 'total time: %.1fs; setup: %.1fs; tests: %.1fs; setup overhead: %.1f%%'
219 227 % (t_all, t_setup, t_done - t_prepared, t_setup / t_all * 100.0)
220 228 )
221 229
222 230
223 231 def run_tests_windows(
224 232 hga: HGAutomation,
225 233 aws_region,
226 234 instance_type,
227 235 python_version,
228 236 arch,
229 237 test_flags,
230 238 base_image_name,
231 239 ):
232 240 c = hga.aws_connection(aws_region)
233 241 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
234 242
235 243 with aws.temporary_windows_dev_instances(
236 244 c, image, instance_type, disable_antivirus=True
237 245 ) as insts:
238 246 instance = insts[0]
239 247
240 248 windows.synchronize_hg(SOURCE_ROOT, '.', instance)
241 249 windows.run_tests(
242 250 instance.winrm_client, python_version, arch, test_flags
243 251 )
244 252
245 253
246 254 def publish_windows_artifacts(
247 255 hg: HGAutomation,
248 256 aws_region,
249 257 version: str,
250 258 pypi: bool,
251 259 mercurial_scm_org: bool,
252 260 ssh_username: str,
253 261 ):
254 262 windows.publish_artifacts(
255 263 DIST_PATH,
256 264 version,
257 265 pypi=pypi,
258 266 mercurial_scm_org=mercurial_scm_org,
259 267 ssh_username=ssh_username,
260 268 )
261 269
262 270
263 271 def run_try(hga: HGAutomation, aws_region: str, rev: str):
264 272 c = hga.aws_connection(aws_region, ensure_ec2_state=False)
265 273 try_server.trigger_try(c, rev=rev)
266 274
267 275
268 276 def get_parser():
269 277 parser = argparse.ArgumentParser()
270 278
271 279 parser.add_argument(
272 280 '--state-path',
273 281 default='~/.hgautomation',
274 282 help='Path for local state files',
275 283 )
276 284 parser.add_argument(
277 285 '--aws-region', help='AWS region to use', default='us-west-2',
278 286 )
279 287
280 288 subparsers = parser.add_subparsers()
281 289
282 290 sp = subparsers.add_parser(
283 291 'bootstrap-linux-dev', help='Bootstrap Linux development environments',
284 292 )
285 293 sp.add_argument(
286 294 '--distros', help='Comma delimited list of distros to bootstrap',
287 295 )
288 296 sp.add_argument(
289 297 '--parallel',
290 298 action='store_true',
291 299 help='Generate AMIs in parallel (not CTRL-c safe)',
292 300 )
293 301 sp.set_defaults(func=bootstrap_linux_dev)
294 302
295 303 sp = subparsers.add_parser(
296 304 'bootstrap-windows-dev',
297 305 help='Bootstrap the Windows development environment',
298 306 )
299 307 sp.add_argument(
300 308 '--base-image-name',
301 309 help='AMI name of base image',
302 310 default=aws.WINDOWS_BASE_IMAGE_NAME,
303 311 )
304 312 sp.set_defaults(func=bootstrap_windows_dev)
305 313
306 314 sp = subparsers.add_parser(
307 315 'build-all-windows-packages', help='Build all Windows packages',
308 316 )
309 317 sp.add_argument(
310 318 '--revision', help='Mercurial revision to build', default='.',
311 319 )
312 320 sp.add_argument(
313 321 '--version', help='Mercurial version string to use',
314 322 )
315 323 sp.add_argument(
316 324 '--base-image-name',
317 325 help='AMI name of base image',
318 326 default=aws.WINDOWS_BASE_IMAGE_NAME,
319 327 )
320 328 sp.set_defaults(func=build_all_windows_packages)
321 329
322 330 sp = subparsers.add_parser(
323 331 'build-inno', help='Build Inno Setup installer(s)',
324 332 )
325 333 sp.add_argument(
326 334 '--python-version',
327 335 help='Which version of Python to target',
328 336 choices={2, 3},
329 337 type=int,
330 338 nargs='*',
331 339 default=[3],
332 340 )
333 341 sp.add_argument(
334 342 '--arch',
335 343 help='Architecture to build for',
336 344 choices={'x86', 'x64'},
337 345 nargs='*',
338 346 default=['x64'],
339 347 )
340 348 sp.add_argument(
341 349 '--revision', help='Mercurial revision to build', default='.',
342 350 )
343 351 sp.add_argument(
344 352 '--version', help='Mercurial version string to use in installer',
345 353 )
346 354 sp.add_argument(
347 355 '--base-image-name',
348 356 help='AMI name of base image',
349 357 default=aws.WINDOWS_BASE_IMAGE_NAME,
350 358 )
351 359 sp.set_defaults(func=build_inno)
352 360
353 361 sp = subparsers.add_parser(
354 362 'build-windows-wheel', help='Build Windows wheel(s)',
355 363 )
356 364 sp.add_argument(
357 365 '--python-version',
358 366 help='Python version to build for',
359 367 choices={'2.7', '3.7', '3.8'},
360 368 nargs='*',
361 369 default=['3.8'],
362 370 )
363 371 sp.add_argument(
364 372 '--arch',
365 373 help='Architecture to build for',
366 374 choices={'x86', 'x64'},
367 375 nargs='*',
368 376 default=['x64'],
369 377 )
370 378 sp.add_argument(
371 379 '--revision', help='Mercurial revision to build', default='.',
372 380 )
373 381 sp.add_argument(
374 382 '--base-image-name',
375 383 help='AMI name of base image',
376 384 default=aws.WINDOWS_BASE_IMAGE_NAME,
377 385 )
378 386 sp.set_defaults(func=build_windows_wheel)
379 387
380 388 sp = subparsers.add_parser('build-wix', help='Build WiX installer(s)')
381 389 sp.add_argument(
390 '--python-version',
391 help='Which version of Python to target',
392 choices={2, 3},
393 type=int,
394 nargs='*',
395 default=[3],
396 )
397 sp.add_argument(
382 398 '--arch',
383 399 help='Architecture to build for',
384 400 choices={'x86', 'x64'},
385 401 nargs='*',
386 402 default=['x64'],
387 403 )
388 404 sp.add_argument(
389 405 '--revision', help='Mercurial revision to build', default='.',
390 406 )
391 407 sp.add_argument(
392 408 '--version', help='Mercurial version string to use in installer',
393 409 )
394 410 sp.add_argument(
395 411 '--base-image-name',
396 412 help='AMI name of base image',
397 413 default=aws.WINDOWS_BASE_IMAGE_NAME,
398 414 )
399 415 sp.set_defaults(func=build_wix)
400 416
401 417 sp = subparsers.add_parser(
402 418 'terminate-ec2-instances',
403 419 help='Terminate all active EC2 instances managed by us',
404 420 )
405 421 sp.set_defaults(func=terminate_ec2_instances)
406 422
407 423 sp = subparsers.add_parser(
408 424 'purge-ec2-resources', help='Purge all EC2 resources managed by us',
409 425 )
410 426 sp.set_defaults(func=purge_ec2_resources)
411 427
412 428 sp = subparsers.add_parser('run-tests-linux', help='Run tests on Linux',)
413 429 sp.add_argument(
414 430 '--distro',
415 431 help='Linux distribution to run tests on',
416 432 choices=linux.DISTROS,
417 433 default='debian10',
418 434 )
419 435 sp.add_argument(
420 436 '--filesystem',
421 437 help='Filesystem type to use',
422 438 choices={'btrfs', 'default', 'ext3', 'ext4', 'jfs', 'tmpfs', 'xfs'},
423 439 default='default',
424 440 )
425 441 sp.add_argument(
426 442 '--instance-type',
427 443 help='EC2 instance type to use',
428 444 default='c5.9xlarge',
429 445 )
430 446 sp.add_argument(
431 447 '--python-version',
432 448 help='Python version to use',
433 449 choices={
434 450 'system2',
435 451 'system3',
436 452 '2.7',
437 453 '3.5',
438 454 '3.6',
439 455 '3.7',
440 456 '3.8',
441 457 'pypy',
442 458 'pypy3.5',
443 459 'pypy3.6',
444 460 },
445 461 default='system2',
446 462 )
447 463 sp.add_argument(
448 464 'test_flags',
449 465 help='Extra command line flags to pass to run-tests.py',
450 466 nargs='*',
451 467 )
452 468 sp.set_defaults(func=run_tests_linux)
453 469
454 470 sp = subparsers.add_parser(
455 471 'run-tests-windows', help='Run tests on Windows',
456 472 )
457 473 sp.add_argument(
458 474 '--instance-type', help='EC2 instance type to use', default='t3.medium',
459 475 )
460 476 sp.add_argument(
461 477 '--python-version',
462 478 help='Python version to use',
463 479 choices={'2.7', '3.5', '3.6', '3.7', '3.8'},
464 480 default='2.7',
465 481 )
466 482 sp.add_argument(
467 483 '--arch',
468 484 help='Architecture to test',
469 485 choices={'x86', 'x64'},
470 486 default='x64',
471 487 )
472 488 sp.add_argument(
473 489 '--test-flags', help='Extra command line flags to pass to run-tests.py',
474 490 )
475 491 sp.add_argument(
476 492 '--base-image-name',
477 493 help='AMI name of base image',
478 494 default=aws.WINDOWS_BASE_IMAGE_NAME,
479 495 )
480 496 sp.set_defaults(func=run_tests_windows)
481 497
482 498 sp = subparsers.add_parser(
483 499 'publish-windows-artifacts',
484 500 help='Publish built Windows artifacts (wheels, installers, etc)',
485 501 )
486 502 sp.add_argument(
487 503 '--no-pypi',
488 504 dest='pypi',
489 505 action='store_false',
490 506 default=True,
491 507 help='Skip uploading to PyPI',
492 508 )
493 509 sp.add_argument(
494 510 '--no-mercurial-scm-org',
495 511 dest='mercurial_scm_org',
496 512 action='store_false',
497 513 default=True,
498 514 help='Skip uploading to www.mercurial-scm.org',
499 515 )
500 516 sp.add_argument(
501 517 '--ssh-username', help='SSH username for mercurial-scm.org',
502 518 )
503 519 sp.add_argument(
504 520 'version', help='Mercurial version string to locate local packages',
505 521 )
506 522 sp.set_defaults(func=publish_windows_artifacts)
507 523
508 524 sp = subparsers.add_parser(
509 525 'try', help='Run CI automation against a custom changeset'
510 526 )
511 527 sp.add_argument('-r', '--rev', default='.', help='Revision to run CI on')
512 528 sp.set_defaults(func=run_try)
513 529
514 530 return parser
515 531
516 532
517 533 def main():
518 534 parser = get_parser()
519 535 args = parser.parse_args()
520 536
521 537 local_state_path = pathlib.Path(os.path.expanduser(args.state_path))
522 538 automation = HGAutomation(local_state_path)
523 539
524 540 if not hasattr(args, 'func'):
525 541 parser.print_help()
526 542 return
527 543
528 544 kwargs = dict(vars(args))
529 545 del kwargs['func']
530 546 del kwargs['state_path']
531 547
532 548 args.func(automation, **kwargs)
@@ -1,598 +1,663 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_PYTHON3 = r'''
72 72 $Env:RUSTUP_HOME = "C:\hgdev\rustup"
73 73 $Env:CARGO_HOME = "C:\hgdev\cargo"
74 74 Set-Location C:\hgdev\src
75 75 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py inno --pyoxidizer-target {pyoxidizer_target} --version {version}
76 76 if ($LASTEXITCODE -ne 0) {{
77 77 throw "process exited non-0: $LASTEXITCODE"
78 78 }}
79 79 '''
80 80
81 81 BUILD_INNO_PYTHON2 = r'''
82 82 Set-Location C:\hgdev\src
83 83 $python = "C:\hgdev\python27-{arch}\python.exe"
84 84 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py inno --python $python {extra_args}
85 85 if ($LASTEXITCODE -ne 0) {{
86 86 throw "process exited non-0: $LASTEXITCODE"
87 87 }}
88 88 '''.lstrip()
89 89
90 90 BUILD_WHEEL = r'''
91 91 Set-Location C:\hgdev\src
92 92 C:\hgdev\python{python_version}-{arch}\python.exe -m pip wheel --wheel-dir dist .
93 93 if ($LASTEXITCODE -ne 0) {{
94 94 throw "process exited non-0: $LASTEXITCODE"
95 95 }}
96 96 '''
97 97
98 BUILD_WIX = r'''
98 BUILD_WIX_PYTHON3 = r'''
99 $Env:RUSTUP_HOME = "C:\hgdev\rustup"
100 $Env:CARGO_HOME = "C:\hgdev\cargo"
101 Set-Location C:\hgdev\src
102 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py wix --pyoxidizer-target {pyoxidizer_target} --version {version}
103 if ($LASTEXITCODE -ne 0) {{
104 throw "process exited non-0: $LASTEXITCODE"
105 }}
106 '''
107
108 BUILD_WIX_PYTHON2 = r'''
99 109 Set-Location C:\hgdev\src
100 110 $python = "C:\hgdev\python27-{arch}\python.exe"
101 111 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py wix --python $python {extra_args}
102 112 if ($LASTEXITCODE -ne 0) {{
103 113 throw "process exited non-0: $LASTEXITCODE"
104 114 }}
105 115 '''
106 116
107 117 RUN_TESTS = r'''
108 118 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}"
109 119 if ($LASTEXITCODE -ne 0) {{
110 120 throw "process exited non-0: $LASTEXITCODE"
111 121 }}
112 122 '''
113 123
114 124 WHEEL_FILENAME_PYTHON27_X86 = 'mercurial-{version}-cp27-cp27m-win32.whl'
115 125 WHEEL_FILENAME_PYTHON27_X64 = 'mercurial-{version}-cp27-cp27m-win_amd64.whl'
116 126 WHEEL_FILENAME_PYTHON37_X86 = 'mercurial-{version}-cp37-cp37m-win32.whl'
117 127 WHEEL_FILENAME_PYTHON37_X64 = 'mercurial-{version}-cp37-cp37m-win_amd64.whl'
118 128 WHEEL_FILENAME_PYTHON38_X86 = 'mercurial-{version}-cp38-cp38-win32.whl'
119 129 WHEEL_FILENAME_PYTHON38_X64 = 'mercurial-{version}-cp38-cp38-win_amd64.whl'
120 130
121 131 EXE_FILENAME_PYTHON2_X86 = 'Mercurial-{version}-x86-python2.exe'
122 132 EXE_FILENAME_PYTHON2_X64 = 'Mercurial-{version}-x64-python2.exe'
123 133 EXE_FILENAME_PYTHON3_X86 = 'Mercurial-{version}-x86.exe'
124 134 EXE_FILENAME_PYTHON3_X64 = 'Mercurial-{version}-x64.exe'
125 X86_MSI_FILENAME = 'mercurial-{version}-x86-python2.msi'
126 X64_MSI_FILENAME = 'mercurial-{version}-x64-python2.msi'
135
136 MSI_FILENAME_PYTHON2_X86 = 'mercurial-{version}-x86-python2.msi'
137 MSI_FILENAME_PYTHON2_X64 = 'mercurial-{version}-x64-python2.msi'
138 MSI_FILENAME_PYTHON3_X86 = 'mercurial-{version}-x86.msi'
139 MSI_FILENAME_PYTHON3_X64 = 'mercurial-{version}-x64.msi'
127 140
128 141 MERCURIAL_SCM_BASE_URL = 'https://mercurial-scm.org/release/windows'
129 142
130 143 X86_USER_AGENT_PATTERN = '.*Windows.*'
131 144 X64_USER_AGENT_PATTERN = '.*Windows.*(WOW|x)64.*'
132 145
133 146 EXE_PYTHON2_X86_DESCRIPTION = (
134 147 'Mercurial {version} Inno Setup installer - x86 Windows (Python 2) '
135 148 '- does not require admin rights'
136 149 )
137 150 EXE_PYTHON2_X64_DESCRIPTION = (
138 151 'Mercurial {version} Inno Setup installer - x64 Windows (Python 2) '
139 152 '- does not require admin rights'
140 153 )
141 154 # TODO remove Python version once Python 2 is dropped.
142 155 EXE_PYTHON3_X86_DESCRIPTION = (
143 156 'Mercurial {version} Inno Setup installer - x86 Windows (Python 3) '
144 157 '- does not require admin rights'
145 158 )
146 159 EXE_PYTHON3_X64_DESCRIPTION = (
147 160 'Mercurial {version} Inno Setup installer - x64 Windows (Python 3) '
148 161 '- does not require admin rights'
149 162 )
150 X86_MSI_DESCRIPTION = (
151 'Mercurial {version} MSI installer - x86 Windows ' '- requires admin rights'
163 MSI_PYTHON2_X86_DESCRIPTION = (
164 'Mercurial {version} MSI installer - x86 Windows (Python 2) '
165 '- requires admin rights'
166 )
167 MSI_PYTHON2_X64_DESCRIPTION = (
168 'Mercurial {version} MSI installer - x64 Windows (Python 2) '
169 '- requires admin rights'
152 170 )
153 X64_MSI_DESCRIPTION = (
154 'Mercurial {version} MSI installer - x64 Windows ' '- requires admin rights'
171 MSI_PYTHON3_X86_DESCRIPTION = (
172 'Mercurial {version} MSI installer - x86 Windows (Python 3) '
173 '- requires admin rights'
174 )
175 MSI_PYTHON3_X64_DESCRIPTION = (
176 'Mercurial {version} MSI installer - x64 Windows (Python 3) '
177 '- requires admin rights'
155 178 )
156 179
157 180
158 181 def get_vc_prefix(arch):
159 182 if arch == 'x86':
160 183 return ACTIVATE_VC9_X86
161 184 elif arch == 'x64':
162 185 return ACTIVATE_VC9_AMD64
163 186 else:
164 187 raise ValueError('illegal arch: %s; must be x86 or x64' % arch)
165 188
166 189
167 190 def fix_authorized_keys_permissions(winrm_client, path):
168 191 commands = [
169 192 '$ErrorActionPreference = "Stop"',
170 193 'Repair-AuthorizedKeyPermission -FilePath %s -Confirm:$false' % path,
171 194 r'icacls %s /remove:g "NT Service\sshd"' % path,
172 195 ]
173 196
174 197 run_powershell(winrm_client, '\n'.join(commands))
175 198
176 199
177 200 def synchronize_hg(hg_repo: pathlib.Path, revision: str, ec2_instance):
178 201 """Synchronize local Mercurial repo to remote EC2 instance."""
179 202
180 203 winrm_client = ec2_instance.winrm_client
181 204
182 205 with tempfile.TemporaryDirectory() as temp_dir:
183 206 temp_dir = pathlib.Path(temp_dir)
184 207
185 208 ssh_dir = temp_dir / '.ssh'
186 209 ssh_dir.mkdir()
187 210 ssh_dir.chmod(0o0700)
188 211
189 212 # Generate SSH key to use for communication.
190 213 subprocess.run(
191 214 [
192 215 'ssh-keygen',
193 216 '-t',
194 217 'rsa',
195 218 '-b',
196 219 '4096',
197 220 '-N',
198 221 '',
199 222 '-f',
200 223 str(ssh_dir / 'id_rsa'),
201 224 ],
202 225 check=True,
203 226 capture_output=True,
204 227 )
205 228
206 229 # Add it to ~/.ssh/authorized_keys on remote.
207 230 # This assumes the file doesn't already exist.
208 231 authorized_keys = r'c:\Users\Administrator\.ssh\authorized_keys'
209 232 winrm_client.execute_cmd(r'mkdir c:\Users\Administrator\.ssh')
210 233 winrm_client.copy(str(ssh_dir / 'id_rsa.pub'), authorized_keys)
211 234 fix_authorized_keys_permissions(winrm_client, authorized_keys)
212 235
213 236 public_ip = ec2_instance.public_ip_address
214 237
215 238 ssh_config = temp_dir / '.ssh' / 'config'
216 239
217 240 with open(ssh_config, 'w', encoding='utf-8') as fh:
218 241 fh.write('Host %s\n' % public_ip)
219 242 fh.write(' User Administrator\n')
220 243 fh.write(' StrictHostKeyChecking no\n')
221 244 fh.write(' UserKnownHostsFile %s\n' % (ssh_dir / 'known_hosts'))
222 245 fh.write(' IdentityFile %s\n' % (ssh_dir / 'id_rsa'))
223 246
224 247 if not (hg_repo / '.hg').is_dir():
225 248 raise Exception(
226 249 '%s is not a Mercurial repository; '
227 250 'synchronization not yet supported' % hg_repo
228 251 )
229 252
230 253 env = dict(os.environ)
231 254 env['HGPLAIN'] = '1'
232 255 env['HGENCODING'] = 'utf-8'
233 256
234 257 hg_bin = hg_repo / 'hg'
235 258
236 259 res = subprocess.run(
237 260 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
238 261 cwd=str(hg_repo),
239 262 env=env,
240 263 check=True,
241 264 capture_output=True,
242 265 )
243 266
244 267 full_revision = res.stdout.decode('ascii')
245 268
246 269 args = [
247 270 'python2.7',
248 271 hg_bin,
249 272 '--config',
250 273 'ui.ssh=ssh -F %s' % ssh_config,
251 274 '--config',
252 275 'ui.remotecmd=c:/hgdev/venv-bootstrap/Scripts/hg.exe',
253 276 # Also ensure .hgtags changes are present so auto version
254 277 # calculation works.
255 278 'push',
256 279 '-f',
257 280 '-r',
258 281 full_revision,
259 282 '-r',
260 283 'file(.hgtags)',
261 284 'ssh://%s/c:/hgdev/src' % public_ip,
262 285 ]
263 286
264 287 res = subprocess.run(args, cwd=str(hg_repo), env=env)
265 288
266 289 # Allow 1 (no-op) to not trigger error.
267 290 if res.returncode not in (0, 1):
268 291 res.check_returncode()
269 292
270 293 run_powershell(
271 294 winrm_client, HG_UPDATE_CLEAN.format(revision=full_revision)
272 295 )
273 296
274 297 # TODO detect dirty local working directory and synchronize accordingly.
275 298
276 299
277 300 def purge_hg(winrm_client):
278 301 """Purge the Mercurial source repository on an EC2 instance."""
279 302 run_powershell(winrm_client, HG_PURGE)
280 303
281 304
282 305 def find_latest_dist(winrm_client, pattern):
283 306 """Find path to newest file in dist/ directory matching a pattern."""
284 307
285 308 res = winrm_client.execute_ps(
286 309 r'$v = Get-ChildItem -Path C:\hgdev\src\dist -Filter "%s" '
287 310 '| Sort-Object LastWriteTime -Descending '
288 311 '| Select-Object -First 1\n'
289 312 '$v.name' % pattern
290 313 )
291 314 return res[0]
292 315
293 316
294 317 def copy_latest_dist(winrm_client, pattern, dest_path):
295 318 """Copy latest file matching pattern in dist/ directory.
296 319
297 320 Given a WinRM client and a file pattern, find the latest file on the remote
298 321 matching that pattern and copy it to the ``dest_path`` directory on the
299 322 local machine.
300 323 """
301 324 latest = find_latest_dist(winrm_client, pattern)
302 325 source = r'C:\hgdev\src\dist\%s' % latest
303 326 dest = dest_path / latest
304 327 print('copying %s to %s' % (source, dest))
305 328 winrm_client.fetch(source, str(dest))
306 329
307 330
308 331 def build_inno_installer(
309 332 winrm_client,
310 333 python_version: int,
311 334 arch: str,
312 335 dest_path: pathlib.Path,
313 336 version=None,
314 337 ):
315 338 """Build the Inno Setup installer on a remote machine.
316 339
317 340 Using a WinRM client, remote commands are executed to build
318 341 a Mercurial Inno Setup installer.
319 342 """
320 343 print(
321 344 'building Inno Setup installer for Python %d %s'
322 345 % (python_version, arch)
323 346 )
324 347
325 348 if python_version == 3:
326 349 # TODO fix this limitation in packaging code
327 350 if not version:
328 351 raise Exception(
329 352 "version string is required when building for Python 3"
330 353 )
331 354
332 355 if arch == "x86":
333 356 target_triple = "i686-pc-windows-msvc"
334 357 elif arch == "x64":
335 358 target_triple = "x86_64-pc-windows-msvc"
336 359 else:
337 360 raise Exception("unhandled arch: %s" % arch)
338 361
339 362 ps = BUILD_INNO_PYTHON3.format(
340 363 pyoxidizer_target=target_triple, version=version,
341 364 )
342 365 else:
343 366 extra_args = []
344 367 if version:
345 368 extra_args.extend(['--version', version])
346 369
347 370 ps = get_vc_prefix(arch) + BUILD_INNO_PYTHON2.format(
348 371 arch=arch, extra_args=' '.join(extra_args)
349 372 )
350 373
351 374 run_powershell(winrm_client, ps)
352 375 copy_latest_dist(winrm_client, '*.exe', dest_path)
353 376
354 377
355 378 def build_wheel(
356 379 winrm_client, python_version: str, arch: str, dest_path: pathlib.Path
357 380 ):
358 381 """Build Python wheels on a remote machine.
359 382
360 383 Using a WinRM client, remote commands are executed to build a Python wheel
361 384 for Mercurial.
362 385 """
363 386 print('Building Windows wheel for Python %s %s' % (python_version, arch))
364 387
365 388 ps = BUILD_WHEEL.format(
366 389 python_version=python_version.replace(".", ""), arch=arch
367 390 )
368 391
369 392 # Python 2.7 requires an activated environment.
370 393 if python_version == "2.7":
371 394 ps = get_vc_prefix(arch) + ps
372 395
373 396 run_powershell(winrm_client, ps)
374 397 copy_latest_dist(winrm_client, '*.whl', dest_path)
375 398
376 399
377 400 def build_wix_installer(
378 winrm_client, arch: str, dest_path: pathlib.Path, version=None
401 winrm_client,
402 python_version: int,
403 arch: str,
404 dest_path: pathlib.Path,
405 version=None,
379 406 ):
380 407 """Build the WiX installer on a remote machine.
381 408
382 409 Using a WinRM client, remote commands are executed to build a WiX installer.
383 410 """
384 print('Building WiX installer for %s' % arch)
385 extra_args = []
386 if version:
387 extra_args.extend(['--version', version])
411 print('Building WiX installer for Python %d %s' % (python_version, arch))
412
413 if python_version == 3:
414 # TODO fix this limitation in packaging code
415 if not version:
416 raise Exception(
417 "version string is required when building for Python 3"
418 )
388 419
389 ps = get_vc_prefix(arch) + BUILD_WIX.format(
390 arch=arch, extra_args=' '.join(extra_args)
391 )
420 if arch == "x86":
421 target_triple = "i686-pc-windows-msvc"
422 elif arch == "x64":
423 target_triple = "x86_64-pc-windows-msvc"
424 else:
425 raise Exception("unhandled arch: %s" % arch)
426
427 ps = BUILD_WIX_PYTHON3.format(
428 pyoxidizer_target=target_triple, version=version,
429 )
430 else:
431 extra_args = []
432 if version:
433 extra_args.extend(['--version', version])
434
435 ps = get_vc_prefix(arch) + BUILD_WIX_PYTHON2.format(
436 arch=arch, extra_args=' '.join(extra_args)
437 )
438
392 439 run_powershell(winrm_client, ps)
393 440 copy_latest_dist(winrm_client, '*.msi', dest_path)
394 441
395 442
396 443 def run_tests(winrm_client, python_version, arch, test_flags=''):
397 444 """Run tests on a remote Windows machine.
398 445
399 446 ``python_version`` is a ``X.Y`` string like ``2.7`` or ``3.7``.
400 447 ``arch`` is ``x86`` or ``x64``.
401 448 ``test_flags`` is a str representing extra arguments to pass to
402 449 ``run-tests.py``.
403 450 """
404 451 if not re.match(r'\d\.\d', python_version):
405 452 raise ValueError(
406 453 r'python_version must be \d.\d; got %s' % python_version
407 454 )
408 455
409 456 if arch not in ('x86', 'x64'):
410 457 raise ValueError('arch must be x86 or x64; got %s' % arch)
411 458
412 459 python_path = 'python%s-%s' % (python_version.replace('.', ''), arch)
413 460
414 461 ps = RUN_TESTS.format(python_path=python_path, test_flags=test_flags or '',)
415 462
416 463 run_powershell(winrm_client, ps)
417 464
418 465
419 466 def resolve_wheel_artifacts(dist_path: pathlib.Path, version: str):
420 467 return (
421 468 dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
422 469 dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
423 470 dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
424 471 dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
425 472 dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
426 473 dist_path / WHEEL_FILENAME_PYTHON38_X64.format(version=version),
427 474 )
428 475
429 476
430 477 def resolve_all_artifacts(dist_path: pathlib.Path, version: str):
431 478 return (
432 479 dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
433 480 dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
434 481 dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
435 482 dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
436 483 dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
437 484 dist_path / WHEEL_FILENAME_PYTHON38_X64.format(version=version),
438 485 dist_path / EXE_FILENAME_PYTHON2_X86.format(version=version),
439 486 dist_path / EXE_FILENAME_PYTHON2_X64.format(version=version),
440 487 dist_path / EXE_FILENAME_PYTHON3_X86.format(version=version),
441 488 dist_path / EXE_FILENAME_PYTHON3_X64.format(version=version),
442 dist_path / X86_MSI_FILENAME.format(version=version),
443 dist_path / X64_MSI_FILENAME.format(version=version),
489 dist_path / MSI_FILENAME_PYTHON2_X86.format(version=version),
490 dist_path / MSI_FILENAME_PYTHON2_X64.format(version=version),
491 dist_path / MSI_FILENAME_PYTHON3_X86.format(version=version),
492 dist_path / MSI_FILENAME_PYTHON3_X64.format(version=version),
444 493 )
445 494
446 495
447 496 def generate_latest_dat(version: str):
448 497 python2_x86_exe_filename = EXE_FILENAME_PYTHON2_X86.format(version=version)
449 498 python2_x64_exe_filename = EXE_FILENAME_PYTHON2_X64.format(version=version)
450 499 python3_x86_exe_filename = EXE_FILENAME_PYTHON3_X86.format(version=version)
451 500 python3_x64_exe_filename = EXE_FILENAME_PYTHON3_X64.format(version=version)
452 x86_msi_filename = X86_MSI_FILENAME.format(version=version)
453 x64_msi_filename = X64_MSI_FILENAME.format(version=version)
501 python2_x86_msi_filename = MSI_FILENAME_PYTHON2_X86.format(version=version)
502 python2_x64_msi_filename = MSI_FILENAME_PYTHON2_X64.format(version=version)
503 python3_x86_msi_filename = MSI_FILENAME_PYTHON3_X86.format(version=version)
504 python3_x64_msi_filename = MSI_FILENAME_PYTHON3_X64.format(version=version)
454 505
455 506 entries = (
456 507 (
457 508 '10',
458 509 version,
459 510 X86_USER_AGENT_PATTERN,
460 511 '%s/%s' % (MERCURIAL_SCM_BASE_URL, python3_x86_exe_filename),
461 512 EXE_PYTHON3_X86_DESCRIPTION.format(version=version),
462 513 ),
463 514 (
464 515 '10',
465 516 version,
466 517 X64_USER_AGENT_PATTERN,
467 518 '%s/%s' % (MERCURIAL_SCM_BASE_URL, python3_x64_exe_filename),
468 519 EXE_PYTHON3_X64_DESCRIPTION.format(version=version),
469 520 ),
470 521 (
471 522 '9',
472 523 version,
473 524 X86_USER_AGENT_PATTERN,
474 525 '%s/%s' % (MERCURIAL_SCM_BASE_URL, python2_x86_exe_filename),
475 526 EXE_PYTHON2_X86_DESCRIPTION.format(version=version),
476 527 ),
477 528 (
478 529 '9',
479 530 version,
480 531 X64_USER_AGENT_PATTERN,
481 532 '%s/%s' % (MERCURIAL_SCM_BASE_URL, python2_x64_exe_filename),
482 533 EXE_PYTHON2_X64_DESCRIPTION.format(version=version),
483 534 ),
484 535 (
485 536 '10',
486 537 version,
487 538 X86_USER_AGENT_PATTERN,
488 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x86_msi_filename),
489 X86_MSI_DESCRIPTION.format(version=version),
539 '%s/%s' % (MERCURIAL_SCM_BASE_URL, python3_x86_msi_filename),
540 MSI_PYTHON3_X86_DESCRIPTION.format(version=version),
490 541 ),
491 542 (
492 543 '10',
493 544 version,
494 545 X64_USER_AGENT_PATTERN,
495 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_msi_filename),
496 X64_MSI_DESCRIPTION.format(version=version),
546 '%s/%s' % (MERCURIAL_SCM_BASE_URL, python3_x64_msi_filename),
547 MSI_PYTHON3_X64_DESCRIPTION.format(version=version),
548 ),
549 (
550 '9',
551 version,
552 X86_USER_AGENT_PATTERN,
553 '%s/%s' % (MERCURIAL_SCM_BASE_URL, python2_x86_msi_filename),
554 MSI_PYTHON2_X86_DESCRIPTION.format(version=version),
555 ),
556 (
557 '9',
558 version,
559 X64_USER_AGENT_PATTERN,
560 '%s/%s' % (MERCURIAL_SCM_BASE_URL, python2_x64_msi_filename),
561 MSI_PYTHON2_X64_DESCRIPTION.format(version=version),
497 562 ),
498 563 )
499 564
500 565 lines = ['\t'.join(e) for e in entries]
501 566
502 567 return '\n'.join(lines) + '\n'
503 568
504 569
505 570 def publish_artifacts_pypi(dist_path: pathlib.Path, version: str):
506 571 """Publish Windows release artifacts to PyPI."""
507 572
508 573 wheel_paths = resolve_wheel_artifacts(dist_path, version)
509 574
510 575 for p in wheel_paths:
511 576 if not p.exists():
512 577 raise Exception('%s not found' % p)
513 578
514 579 print('uploading wheels to PyPI (you may be prompted for credentials)')
515 580 pypi_upload(wheel_paths)
516 581
517 582
518 583 def publish_artifacts_mercurial_scm_org(
519 584 dist_path: pathlib.Path, version: str, ssh_username=None
520 585 ):
521 586 """Publish Windows release artifacts to mercurial-scm.org."""
522 587 all_paths = resolve_all_artifacts(dist_path, version)
523 588
524 589 for p in all_paths:
525 590 if not p.exists():
526 591 raise Exception('%s not found' % p)
527 592
528 593 client = paramiko.SSHClient()
529 594 client.load_system_host_keys()
530 595 # We assume the system SSH configuration knows how to connect.
531 596 print('connecting to mercurial-scm.org via ssh...')
532 597 try:
533 598 client.connect('mercurial-scm.org', username=ssh_username)
534 599 except paramiko.AuthenticationException:
535 600 print('error authenticating; is an SSH key available in an SSH agent?')
536 601 raise
537 602
538 603 print('SSH connection established')
539 604
540 605 print('opening SFTP client...')
541 606 sftp = client.open_sftp()
542 607 print('SFTP client obtained')
543 608
544 609 for p in all_paths:
545 610 dest_path = '/var/www/release/windows/%s' % p.name
546 611 print('uploading %s to %s' % (p, dest_path))
547 612
548 613 with p.open('rb') as fh:
549 614 data = fh.read()
550 615
551 616 with sftp.open(dest_path, 'wb') as fh:
552 617 fh.write(data)
553 618 fh.chmod(0o0664)
554 619
555 620 latest_dat_path = '/var/www/release/windows/latest.dat'
556 621
557 622 now = datetime.datetime.utcnow()
558 623 backup_path = dist_path / (
559 624 'latest-windows-%s.dat' % now.strftime('%Y%m%dT%H%M%S')
560 625 )
561 626 print('backing up %s to %s' % (latest_dat_path, backup_path))
562 627
563 628 with sftp.open(latest_dat_path, 'rb') as fh:
564 629 latest_dat_old = fh.read()
565 630
566 631 with backup_path.open('wb') as fh:
567 632 fh.write(latest_dat_old)
568 633
569 634 print('writing %s with content:' % latest_dat_path)
570 635 latest_dat_content = generate_latest_dat(version)
571 636 print(latest_dat_content)
572 637
573 638 with sftp.open(latest_dat_path, 'wb') as fh:
574 639 fh.write(latest_dat_content.encode('ascii'))
575 640
576 641
577 642 def publish_artifacts(
578 643 dist_path: pathlib.Path,
579 644 version: str,
580 645 pypi=True,
581 646 mercurial_scm_org=True,
582 647 ssh_username=None,
583 648 ):
584 649 """Publish Windows release artifacts.
585 650
586 651 Files are found in `dist_path`. We will look for files with version string
587 652 `version`.
588 653
589 654 `pypi` controls whether we upload to PyPI.
590 655 `mercurial_scm_org` controls whether we upload to mercurial-scm.org.
591 656 """
592 657 if pypi:
593 658 publish_artifacts_pypi(dist_path, version)
594 659
595 660 if mercurial_scm_org:
596 661 publish_artifacts_mercurial_scm_org(
597 662 dist_path, version, ssh_username=ssh_username
598 663 )
General Comments 0
You need to be logged in to leave comments. Login now