##// END OF EJS Templates
automation: support building Windows wheels for Python 3.7 and 3.8...
Gregory Szorc -
r45261:48096e26 default draft
parent child Browse files
Show More
@@ -1,487 +1,510 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, aws_region, arch, revision, version, base_image_name
67 67 ):
68 68 c = hga.aws_connection(aws_region)
69 69 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
70 70 DIST_PATH.mkdir(exist_ok=True)
71 71
72 72 with aws.temporary_windows_dev_instances(c, image, 't3.medium') as insts:
73 73 instance = insts[0]
74 74
75 75 windows.synchronize_hg(SOURCE_ROOT, revision, instance)
76 76
77 77 for a in arch:
78 78 windows.build_inno_installer(
79 79 instance.winrm_client, a, DIST_PATH, version=version
80 80 )
81 81
82 82
83 83 def build_wix(
84 84 hga: HGAutomation, aws_region, arch, revision, version, base_image_name
85 85 ):
86 86 c = hga.aws_connection(aws_region)
87 87 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
88 88 DIST_PATH.mkdir(exist_ok=True)
89 89
90 90 with aws.temporary_windows_dev_instances(c, image, 't3.medium') as insts:
91 91 instance = insts[0]
92 92
93 93 windows.synchronize_hg(SOURCE_ROOT, revision, instance)
94 94
95 95 for a in arch:
96 96 windows.build_wix_installer(
97 97 instance.winrm_client, a, DIST_PATH, version=version
98 98 )
99 99
100 100
101 101 def build_windows_wheel(
102 hga: HGAutomation, aws_region, arch, revision, base_image_name
102 hga: HGAutomation,
103 aws_region,
104 python_version,
105 arch,
106 revision,
107 base_image_name,
103 108 ):
104 109 c = hga.aws_connection(aws_region)
105 110 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
106 111 DIST_PATH.mkdir(exist_ok=True)
107 112
108 113 with aws.temporary_windows_dev_instances(c, image, 't3.medium') as insts:
109 114 instance = insts[0]
110 115
111 116 windows.synchronize_hg(SOURCE_ROOT, revision, instance)
112 117
113 for a in arch:
114 windows.build_wheel(instance.winrm_client, a, DIST_PATH)
118 for py_version in python_version:
119 for a in arch:
120 windows.build_wheel(
121 instance.winrm_client, py_version, a, DIST_PATH
122 )
115 123
116 124
117 125 def build_all_windows_packages(
118 126 hga: HGAutomation, aws_region, revision, version, base_image_name
119 127 ):
120 128 c = hga.aws_connection(aws_region)
121 129 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
122 130 DIST_PATH.mkdir(exist_ok=True)
123 131
124 132 with aws.temporary_windows_dev_instances(c, image, 't3.medium') as insts:
125 133 instance = insts[0]
126 134
127 135 winrm_client = instance.winrm_client
128 136
129 137 windows.synchronize_hg(SOURCE_ROOT, revision, instance)
130 138
139 for py_version in ("2.7", "3.7", "3.8"):
140 for arch in ("x86", "x64"):
141 windows.purge_hg(winrm_client)
142 windows.build_wheel(
143 winrm_client,
144 python_version=py_version,
145 arch=arch,
146 dest_path=DIST_PATH,
147 )
148
131 149 for arch in ('x86', 'x64'):
132 150 windows.purge_hg(winrm_client)
133 windows.build_wheel(winrm_client, arch, DIST_PATH)
134 windows.purge_hg(winrm_client)
135 151 windows.build_inno_installer(
136 152 winrm_client, arch, DIST_PATH, version=version
137 153 )
138 154 windows.purge_hg(winrm_client)
139 155 windows.build_wix_installer(
140 156 winrm_client, arch, DIST_PATH, version=version
141 157 )
142 158
143 159
144 160 def terminate_ec2_instances(hga: HGAutomation, aws_region):
145 161 c = hga.aws_connection(aws_region, ensure_ec2_state=False)
146 162 aws.terminate_ec2_instances(c.ec2resource)
147 163
148 164
149 165 def purge_ec2_resources(hga: HGAutomation, aws_region):
150 166 c = hga.aws_connection(aws_region, ensure_ec2_state=False)
151 167 aws.remove_resources(c)
152 168
153 169
154 170 def run_tests_linux(
155 171 hga: HGAutomation,
156 172 aws_region,
157 173 instance_type,
158 174 python_version,
159 175 test_flags,
160 176 distro,
161 177 filesystem,
162 178 ):
163 179 c = hga.aws_connection(aws_region)
164 180 image = aws.ensure_linux_dev_ami(c, distro=distro)
165 181
166 182 t_start = time.time()
167 183
168 184 ensure_extra_volume = filesystem not in ('default', 'tmpfs')
169 185
170 186 with aws.temporary_linux_dev_instances(
171 187 c, image, instance_type, ensure_extra_volume=ensure_extra_volume
172 188 ) as insts:
173 189
174 190 instance = insts[0]
175 191
176 192 linux.prepare_exec_environment(
177 193 instance.ssh_client, filesystem=filesystem
178 194 )
179 195 linux.synchronize_hg(SOURCE_ROOT, instance, '.')
180 196 t_prepared = time.time()
181 197 linux.run_tests(instance.ssh_client, python_version, test_flags)
182 198 t_done = time.time()
183 199
184 200 t_setup = t_prepared - t_start
185 201 t_all = t_done - t_start
186 202
187 203 print(
188 204 'total time: %.1fs; setup: %.1fs; tests: %.1fs; setup overhead: %.1f%%'
189 205 % (t_all, t_setup, t_done - t_prepared, t_setup / t_all * 100.0)
190 206 )
191 207
192 208
193 209 def run_tests_windows(
194 210 hga: HGAutomation,
195 211 aws_region,
196 212 instance_type,
197 213 python_version,
198 214 arch,
199 215 test_flags,
200 216 base_image_name,
201 217 ):
202 218 c = hga.aws_connection(aws_region)
203 219 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
204 220
205 221 with aws.temporary_windows_dev_instances(
206 222 c, image, instance_type, disable_antivirus=True
207 223 ) as insts:
208 224 instance = insts[0]
209 225
210 226 windows.synchronize_hg(SOURCE_ROOT, '.', instance)
211 227 windows.run_tests(
212 228 instance.winrm_client, python_version, arch, test_flags
213 229 )
214 230
215 231
216 232 def publish_windows_artifacts(
217 233 hg: HGAutomation,
218 234 aws_region,
219 235 version: str,
220 236 pypi: bool,
221 237 mercurial_scm_org: bool,
222 238 ssh_username: str,
223 239 ):
224 240 windows.publish_artifacts(
225 241 DIST_PATH,
226 242 version,
227 243 pypi=pypi,
228 244 mercurial_scm_org=mercurial_scm_org,
229 245 ssh_username=ssh_username,
230 246 )
231 247
232 248
233 249 def run_try(hga: HGAutomation, aws_region: str, rev: str):
234 250 c = hga.aws_connection(aws_region, ensure_ec2_state=False)
235 251 try_server.trigger_try(c, rev=rev)
236 252
237 253
238 254 def get_parser():
239 255 parser = argparse.ArgumentParser()
240 256
241 257 parser.add_argument(
242 258 '--state-path',
243 259 default='~/.hgautomation',
244 260 help='Path for local state files',
245 261 )
246 262 parser.add_argument(
247 263 '--aws-region', help='AWS region to use', default='us-west-2',
248 264 )
249 265
250 266 subparsers = parser.add_subparsers()
251 267
252 268 sp = subparsers.add_parser(
253 269 'bootstrap-linux-dev', help='Bootstrap Linux development environments',
254 270 )
255 271 sp.add_argument(
256 272 '--distros', help='Comma delimited list of distros to bootstrap',
257 273 )
258 274 sp.add_argument(
259 275 '--parallel',
260 276 action='store_true',
261 277 help='Generate AMIs in parallel (not CTRL-c safe)',
262 278 )
263 279 sp.set_defaults(func=bootstrap_linux_dev)
264 280
265 281 sp = subparsers.add_parser(
266 282 'bootstrap-windows-dev',
267 283 help='Bootstrap the Windows development environment',
268 284 )
269 285 sp.add_argument(
270 286 '--base-image-name',
271 287 help='AMI name of base image',
272 288 default=aws.WINDOWS_BASE_IMAGE_NAME,
273 289 )
274 290 sp.set_defaults(func=bootstrap_windows_dev)
275 291
276 292 sp = subparsers.add_parser(
277 293 'build-all-windows-packages', help='Build all Windows packages',
278 294 )
279 295 sp.add_argument(
280 296 '--revision', help='Mercurial revision to build', default='.',
281 297 )
282 298 sp.add_argument(
283 299 '--version', help='Mercurial version string to use',
284 300 )
285 301 sp.add_argument(
286 302 '--base-image-name',
287 303 help='AMI name of base image',
288 304 default=aws.WINDOWS_BASE_IMAGE_NAME,
289 305 )
290 306 sp.set_defaults(func=build_all_windows_packages)
291 307
292 308 sp = subparsers.add_parser(
293 309 'build-inno', help='Build Inno Setup installer(s)',
294 310 )
295 311 sp.add_argument(
296 312 '--arch',
297 313 help='Architecture to build for',
298 314 choices={'x86', 'x64'},
299 315 nargs='*',
300 316 default=['x64'],
301 317 )
302 318 sp.add_argument(
303 319 '--revision', help='Mercurial revision to build', default='.',
304 320 )
305 321 sp.add_argument(
306 322 '--version', help='Mercurial version string to use in installer',
307 323 )
308 324 sp.add_argument(
309 325 '--base-image-name',
310 326 help='AMI name of base image',
311 327 default=aws.WINDOWS_BASE_IMAGE_NAME,
312 328 )
313 329 sp.set_defaults(func=build_inno)
314 330
315 331 sp = subparsers.add_parser(
316 332 'build-windows-wheel', help='Build Windows wheel(s)',
317 333 )
318 334 sp.add_argument(
335 '--python-version',
336 help='Python version to build for',
337 choices={'2.7', '3.7', '3.8'},
338 nargs='*',
339 default=['3.8'],
340 )
341 sp.add_argument(
319 342 '--arch',
320 343 help='Architecture to build for',
321 344 choices={'x86', 'x64'},
322 345 nargs='*',
323 346 default=['x64'],
324 347 )
325 348 sp.add_argument(
326 349 '--revision', help='Mercurial revision to build', default='.',
327 350 )
328 351 sp.add_argument(
329 352 '--base-image-name',
330 353 help='AMI name of base image',
331 354 default=aws.WINDOWS_BASE_IMAGE_NAME,
332 355 )
333 356 sp.set_defaults(func=build_windows_wheel)
334 357
335 358 sp = subparsers.add_parser('build-wix', help='Build WiX installer(s)')
336 359 sp.add_argument(
337 360 '--arch',
338 361 help='Architecture to build for',
339 362 choices={'x86', 'x64'},
340 363 nargs='*',
341 364 default=['x64'],
342 365 )
343 366 sp.add_argument(
344 367 '--revision', help='Mercurial revision to build', default='.',
345 368 )
346 369 sp.add_argument(
347 370 '--version', help='Mercurial version string to use in installer',
348 371 )
349 372 sp.add_argument(
350 373 '--base-image-name',
351 374 help='AMI name of base image',
352 375 default=aws.WINDOWS_BASE_IMAGE_NAME,
353 376 )
354 377 sp.set_defaults(func=build_wix)
355 378
356 379 sp = subparsers.add_parser(
357 380 'terminate-ec2-instances',
358 381 help='Terminate all active EC2 instances managed by us',
359 382 )
360 383 sp.set_defaults(func=terminate_ec2_instances)
361 384
362 385 sp = subparsers.add_parser(
363 386 'purge-ec2-resources', help='Purge all EC2 resources managed by us',
364 387 )
365 388 sp.set_defaults(func=purge_ec2_resources)
366 389
367 390 sp = subparsers.add_parser('run-tests-linux', help='Run tests on Linux',)
368 391 sp.add_argument(
369 392 '--distro',
370 393 help='Linux distribution to run tests on',
371 394 choices=linux.DISTROS,
372 395 default='debian10',
373 396 )
374 397 sp.add_argument(
375 398 '--filesystem',
376 399 help='Filesystem type to use',
377 400 choices={'btrfs', 'default', 'ext3', 'ext4', 'jfs', 'tmpfs', 'xfs'},
378 401 default='default',
379 402 )
380 403 sp.add_argument(
381 404 '--instance-type',
382 405 help='EC2 instance type to use',
383 406 default='c5.9xlarge',
384 407 )
385 408 sp.add_argument(
386 409 '--python-version',
387 410 help='Python version to use',
388 411 choices={
389 412 'system2',
390 413 'system3',
391 414 '2.7',
392 415 '3.5',
393 416 '3.6',
394 417 '3.7',
395 418 '3.8',
396 419 'pypy',
397 420 'pypy3.5',
398 421 'pypy3.6',
399 422 },
400 423 default='system2',
401 424 )
402 425 sp.add_argument(
403 426 'test_flags',
404 427 help='Extra command line flags to pass to run-tests.py',
405 428 nargs='*',
406 429 )
407 430 sp.set_defaults(func=run_tests_linux)
408 431
409 432 sp = subparsers.add_parser(
410 433 'run-tests-windows', help='Run tests on Windows',
411 434 )
412 435 sp.add_argument(
413 436 '--instance-type', help='EC2 instance type to use', default='t3.medium',
414 437 )
415 438 sp.add_argument(
416 439 '--python-version',
417 440 help='Python version to use',
418 441 choices={'2.7', '3.5', '3.6', '3.7', '3.8'},
419 442 default='2.7',
420 443 )
421 444 sp.add_argument(
422 445 '--arch',
423 446 help='Architecture to test',
424 447 choices={'x86', 'x64'},
425 448 default='x64',
426 449 )
427 450 sp.add_argument(
428 451 '--test-flags', help='Extra command line flags to pass to run-tests.py',
429 452 )
430 453 sp.add_argument(
431 454 '--base-image-name',
432 455 help='AMI name of base image',
433 456 default=aws.WINDOWS_BASE_IMAGE_NAME,
434 457 )
435 458 sp.set_defaults(func=run_tests_windows)
436 459
437 460 sp = subparsers.add_parser(
438 461 'publish-windows-artifacts',
439 462 help='Publish built Windows artifacts (wheels, installers, etc)',
440 463 )
441 464 sp.add_argument(
442 465 '--no-pypi',
443 466 dest='pypi',
444 467 action='store_false',
445 468 default=True,
446 469 help='Skip uploading to PyPI',
447 470 )
448 471 sp.add_argument(
449 472 '--no-mercurial-scm-org',
450 473 dest='mercurial_scm_org',
451 474 action='store_false',
452 475 default=True,
453 476 help='Skip uploading to www.mercurial-scm.org',
454 477 )
455 478 sp.add_argument(
456 479 '--ssh-username', help='SSH username for mercurial-scm.org',
457 480 )
458 481 sp.add_argument(
459 482 'version', help='Mercurial version string to locate local packages',
460 483 )
461 484 sp.set_defaults(func=publish_windows_artifacts)
462 485
463 486 sp = subparsers.add_parser(
464 487 'try', help='Run CI automation against a custom changeset'
465 488 )
466 489 sp.add_argument('-r', '--rev', default='.', help='Revision to run CI on')
467 490 sp.set_defaults(func=run_try)
468 491
469 492 return parser
470 493
471 494
472 495 def main():
473 496 parser = get_parser()
474 497 args = parser.parse_args()
475 498
476 499 local_state_path = pathlib.Path(os.path.expanduser(args.state_path))
477 500 automation = HGAutomation(local_state_path)
478 501
479 502 if not hasattr(args, 'func'):
480 503 parser.print_help()
481 504 return
482 505
483 506 kwargs = dict(vars(args))
484 507 del kwargs['func']
485 508 del kwargs['state_path']
486 509
487 510 args.func(automation, **kwargs)
@@ -1,510 +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 C:\hgdev\python27-{arch}\Scripts\pip.exe wheel --wheel-dir dist .
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 X86_WHEEL_FILENAME = 'mercurial-{version}-cp27-cp27m-win32.whl'
105 X64_WHEEL_FILENAME = 'mercurial-{version}-cp27-cp27m-win_amd64.whl'
104 WHEEL_FILENAME_PYTHON27_X86 = 'mercurial-{version}-cp27-cp27m-win32.whl'
105 WHEEL_FILENAME_PYTHON27_X64 = 'mercurial-{version}-cp27-cp27m-win_amd64.whl'
106 WHEEL_FILENAME_PYTHON37_X86 = 'mercurial-{version}-cp37-cp37m-win32.whl'
107 WHEEL_FILENAME_PYTHON37_X64 = 'mercurial-{version}-cp37-cp37m-win_amd64.whl'
108 WHEEL_FILENAME_PYTHON38_X86 = 'mercurial-{version}-cp38-cp38-win32.whl'
109 WHEEL_FILENAME_PYTHON38_X64 = 'mercurial-{version}-cp38-cp38-win_amd64.whl'
110
106 111 X86_EXE_FILENAME = 'Mercurial-{version}.exe'
107 112 X64_EXE_FILENAME = 'Mercurial-{version}-x64.exe'
108 113 X86_MSI_FILENAME = 'mercurial-{version}-x86.msi'
109 114 X64_MSI_FILENAME = 'mercurial-{version}-x64.msi'
110 115
111 116 MERCURIAL_SCM_BASE_URL = 'https://mercurial-scm.org/release/windows'
112 117
113 118 X86_USER_AGENT_PATTERN = '.*Windows.*'
114 119 X64_USER_AGENT_PATTERN = '.*Windows.*(WOW|x)64.*'
115 120
116 121 X86_EXE_DESCRIPTION = (
117 122 'Mercurial {version} Inno Setup installer - x86 Windows '
118 123 '- does not require admin rights'
119 124 )
120 125 X64_EXE_DESCRIPTION = (
121 126 'Mercurial {version} Inno Setup installer - x64 Windows '
122 127 '- does not require admin rights'
123 128 )
124 129 X86_MSI_DESCRIPTION = (
125 130 'Mercurial {version} MSI installer - x86 Windows ' '- requires admin rights'
126 131 )
127 132 X64_MSI_DESCRIPTION = (
128 133 'Mercurial {version} MSI installer - x64 Windows ' '- requires admin rights'
129 134 )
130 135
131 136
132 137 def get_vc_prefix(arch):
133 138 if arch == 'x86':
134 139 return ACTIVATE_VC9_X86
135 140 elif arch == 'x64':
136 141 return ACTIVATE_VC9_AMD64
137 142 else:
138 143 raise ValueError('illegal arch: %s; must be x86 or x64' % arch)
139 144
140 145
141 146 def fix_authorized_keys_permissions(winrm_client, path):
142 147 commands = [
143 148 '$ErrorActionPreference = "Stop"',
144 149 'Repair-AuthorizedKeyPermission -FilePath %s -Confirm:$false' % path,
145 150 r'icacls %s /remove:g "NT Service\sshd"' % path,
146 151 ]
147 152
148 153 run_powershell(winrm_client, '\n'.join(commands))
149 154
150 155
151 156 def synchronize_hg(hg_repo: pathlib.Path, revision: str, ec2_instance):
152 157 """Synchronize local Mercurial repo to remote EC2 instance."""
153 158
154 159 winrm_client = ec2_instance.winrm_client
155 160
156 161 with tempfile.TemporaryDirectory() as temp_dir:
157 162 temp_dir = pathlib.Path(temp_dir)
158 163
159 164 ssh_dir = temp_dir / '.ssh'
160 165 ssh_dir.mkdir()
161 166 ssh_dir.chmod(0o0700)
162 167
163 168 # Generate SSH key to use for communication.
164 169 subprocess.run(
165 170 [
166 171 'ssh-keygen',
167 172 '-t',
168 173 'rsa',
169 174 '-b',
170 175 '4096',
171 176 '-N',
172 177 '',
173 178 '-f',
174 179 str(ssh_dir / 'id_rsa'),
175 180 ],
176 181 check=True,
177 182 capture_output=True,
178 183 )
179 184
180 185 # Add it to ~/.ssh/authorized_keys on remote.
181 186 # This assumes the file doesn't already exist.
182 187 authorized_keys = r'c:\Users\Administrator\.ssh\authorized_keys'
183 188 winrm_client.execute_cmd(r'mkdir c:\Users\Administrator\.ssh')
184 189 winrm_client.copy(str(ssh_dir / 'id_rsa.pub'), authorized_keys)
185 190 fix_authorized_keys_permissions(winrm_client, authorized_keys)
186 191
187 192 public_ip = ec2_instance.public_ip_address
188 193
189 194 ssh_config = temp_dir / '.ssh' / 'config'
190 195
191 196 with open(ssh_config, 'w', encoding='utf-8') as fh:
192 197 fh.write('Host %s\n' % public_ip)
193 198 fh.write(' User Administrator\n')
194 199 fh.write(' StrictHostKeyChecking no\n')
195 200 fh.write(' UserKnownHostsFile %s\n' % (ssh_dir / 'known_hosts'))
196 201 fh.write(' IdentityFile %s\n' % (ssh_dir / 'id_rsa'))
197 202
198 203 if not (hg_repo / '.hg').is_dir():
199 204 raise Exception(
200 205 '%s is not a Mercurial repository; '
201 206 'synchronization not yet supported' % hg_repo
202 207 )
203 208
204 209 env = dict(os.environ)
205 210 env['HGPLAIN'] = '1'
206 211 env['HGENCODING'] = 'utf-8'
207 212
208 213 hg_bin = hg_repo / 'hg'
209 214
210 215 res = subprocess.run(
211 216 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
212 217 cwd=str(hg_repo),
213 218 env=env,
214 219 check=True,
215 220 capture_output=True,
216 221 )
217 222
218 223 full_revision = res.stdout.decode('ascii')
219 224
220 225 args = [
221 226 'python2.7',
222 227 hg_bin,
223 228 '--config',
224 229 'ui.ssh=ssh -F %s' % ssh_config,
225 230 '--config',
226 231 'ui.remotecmd=c:/hgdev/venv-bootstrap/Scripts/hg.exe',
227 232 # Also ensure .hgtags changes are present so auto version
228 233 # calculation works.
229 234 'push',
230 235 '-f',
231 236 '-r',
232 237 full_revision,
233 238 '-r',
234 239 'file(.hgtags)',
235 240 'ssh://%s/c:/hgdev/src' % public_ip,
236 241 ]
237 242
238 243 res = subprocess.run(args, cwd=str(hg_repo), env=env)
239 244
240 245 # Allow 1 (no-op) to not trigger error.
241 246 if res.returncode not in (0, 1):
242 247 res.check_returncode()
243 248
244 249 run_powershell(
245 250 winrm_client, HG_UPDATE_CLEAN.format(revision=full_revision)
246 251 )
247 252
248 253 # TODO detect dirty local working directory and synchronize accordingly.
249 254
250 255
251 256 def purge_hg(winrm_client):
252 257 """Purge the Mercurial source repository on an EC2 instance."""
253 258 run_powershell(winrm_client, HG_PURGE)
254 259
255 260
256 261 def find_latest_dist(winrm_client, pattern):
257 262 """Find path to newest file in dist/ directory matching a pattern."""
258 263
259 264 res = winrm_client.execute_ps(
260 265 r'$v = Get-ChildItem -Path C:\hgdev\src\dist -Filter "%s" '
261 266 '| Sort-Object LastWriteTime -Descending '
262 267 '| Select-Object -First 1\n'
263 268 '$v.name' % pattern
264 269 )
265 270 return res[0]
266 271
267 272
268 273 def copy_latest_dist(winrm_client, pattern, dest_path):
269 274 """Copy latest file matching pattern in dist/ directory.
270 275
271 276 Given a WinRM client and a file pattern, find the latest file on the remote
272 277 matching that pattern and copy it to the ``dest_path`` directory on the
273 278 local machine.
274 279 """
275 280 latest = find_latest_dist(winrm_client, pattern)
276 281 source = r'C:\hgdev\src\dist\%s' % latest
277 282 dest = dest_path / latest
278 283 print('copying %s to %s' % (source, dest))
279 284 winrm_client.fetch(source, str(dest))
280 285
281 286
282 287 def build_inno_installer(
283 288 winrm_client, arch: str, dest_path: pathlib.Path, version=None
284 289 ):
285 290 """Build the Inno Setup installer on a remote machine.
286 291
287 292 Using a WinRM client, remote commands are executed to build
288 293 a Mercurial Inno Setup installer.
289 294 """
290 295 print('building Inno Setup installer for %s' % arch)
291 296
292 297 extra_args = []
293 298 if version:
294 299 extra_args.extend(['--version', version])
295 300
296 301 ps = get_vc_prefix(arch) + BUILD_INNO.format(
297 302 arch=arch, extra_args=' '.join(extra_args)
298 303 )
299 304 run_powershell(winrm_client, ps)
300 305 copy_latest_dist(winrm_client, '*.exe', dest_path)
301 306
302 307
303 def build_wheel(winrm_client, arch: str, dest_path: pathlib.Path):
308 def build_wheel(
309 winrm_client, python_version: str, arch: str, dest_path: pathlib.Path
310 ):
304 311 """Build Python wheels on a remote machine.
305 312
306 313 Using a WinRM client, remote commands are executed to build a Python wheel
307 314 for Mercurial.
308 315 """
309 print('Building Windows wheel for %s' % arch)
310 ps = get_vc_prefix(arch) + BUILD_WHEEL.format(arch=arch)
316 print('Building Windows wheel for Python %s %s' % (python_version, arch))
317
318 ps = BUILD_WHEEL.format(
319 python_version=python_version.replace(".", ""), arch=arch
320 )
321
322 # Python 2.7 requires an activated environment.
323 if python_version == "2.7":
324 ps = get_vc_prefix(arch) + ps
325
311 326 run_powershell(winrm_client, ps)
312 327 copy_latest_dist(winrm_client, '*.whl', dest_path)
313 328
314 329
315 330 def build_wix_installer(
316 331 winrm_client, arch: str, dest_path: pathlib.Path, version=None
317 332 ):
318 333 """Build the WiX installer on a remote machine.
319 334
320 335 Using a WinRM client, remote commands are executed to build a WiX installer.
321 336 """
322 337 print('Building WiX installer for %s' % arch)
323 338 extra_args = []
324 339 if version:
325 340 extra_args.extend(['--version', version])
326 341
327 342 ps = get_vc_prefix(arch) + BUILD_WIX.format(
328 343 arch=arch, extra_args=' '.join(extra_args)
329 344 )
330 345 run_powershell(winrm_client, ps)
331 346 copy_latest_dist(winrm_client, '*.msi', dest_path)
332 347
333 348
334 349 def run_tests(winrm_client, python_version, arch, test_flags=''):
335 350 """Run tests on a remote Windows machine.
336 351
337 352 ``python_version`` is a ``X.Y`` string like ``2.7`` or ``3.7``.
338 353 ``arch`` is ``x86`` or ``x64``.
339 354 ``test_flags`` is a str representing extra arguments to pass to
340 355 ``run-tests.py``.
341 356 """
342 357 if not re.match(r'\d\.\d', python_version):
343 358 raise ValueError(
344 359 r'python_version must be \d.\d; got %s' % python_version
345 360 )
346 361
347 362 if arch not in ('x86', 'x64'):
348 363 raise ValueError('arch must be x86 or x64; got %s' % arch)
349 364
350 365 python_path = 'python%s-%s' % (python_version.replace('.', ''), arch)
351 366
352 367 ps = RUN_TESTS.format(python_path=python_path, test_flags=test_flags or '',)
353 368
354 369 run_powershell(winrm_client, ps)
355 370
356 371
357 372 def resolve_wheel_artifacts(dist_path: pathlib.Path, version: str):
358 373 return (
359 dist_path / X86_WHEEL_FILENAME.format(version=version),
360 dist_path / X64_WHEEL_FILENAME.format(version=version),
374 dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
375 dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
376 dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
377 dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
378 dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
379 dist_path / WHEEL_FILENAME_PYTHON38_X64.format(version=version),
361 380 )
362 381
363 382
364 383 def resolve_all_artifacts(dist_path: pathlib.Path, version: str):
365 384 return (
366 dist_path / X86_WHEEL_FILENAME.format(version=version),
367 dist_path / X64_WHEEL_FILENAME.format(version=version),
385 dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
386 dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
387 dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
388 dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
389 dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
390 dist_path / WHEEL_FILENAME_PYTHON38_X64.format(version=version),
368 391 dist_path / X86_EXE_FILENAME.format(version=version),
369 392 dist_path / X64_EXE_FILENAME.format(version=version),
370 393 dist_path / X86_MSI_FILENAME.format(version=version),
371 394 dist_path / X64_MSI_FILENAME.format(version=version),
372 395 )
373 396
374 397
375 398 def generate_latest_dat(version: str):
376 399 x86_exe_filename = X86_EXE_FILENAME.format(version=version)
377 400 x64_exe_filename = X64_EXE_FILENAME.format(version=version)
378 401 x86_msi_filename = X86_MSI_FILENAME.format(version=version)
379 402 x64_msi_filename = X64_MSI_FILENAME.format(version=version)
380 403
381 404 entries = (
382 405 (
383 406 '10',
384 407 version,
385 408 X86_USER_AGENT_PATTERN,
386 409 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x86_exe_filename),
387 410 X86_EXE_DESCRIPTION.format(version=version),
388 411 ),
389 412 (
390 413 '10',
391 414 version,
392 415 X64_USER_AGENT_PATTERN,
393 416 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_exe_filename),
394 417 X64_EXE_DESCRIPTION.format(version=version),
395 418 ),
396 419 (
397 420 '10',
398 421 version,
399 422 X86_USER_AGENT_PATTERN,
400 423 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x86_msi_filename),
401 424 X86_MSI_DESCRIPTION.format(version=version),
402 425 ),
403 426 (
404 427 '10',
405 428 version,
406 429 X64_USER_AGENT_PATTERN,
407 430 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_msi_filename),
408 431 X64_MSI_DESCRIPTION.format(version=version),
409 432 ),
410 433 )
411 434
412 435 lines = ['\t'.join(e) for e in entries]
413 436
414 437 return '\n'.join(lines) + '\n'
415 438
416 439
417 440 def publish_artifacts_pypi(dist_path: pathlib.Path, version: str):
418 441 """Publish Windows release artifacts to PyPI."""
419 442
420 443 wheel_paths = resolve_wheel_artifacts(dist_path, version)
421 444
422 445 for p in wheel_paths:
423 446 if not p.exists():
424 447 raise Exception('%s not found' % p)
425 448
426 449 print('uploading wheels to PyPI (you may be prompted for credentials)')
427 450 pypi_upload(wheel_paths)
428 451
429 452
430 453 def publish_artifacts_mercurial_scm_org(
431 454 dist_path: pathlib.Path, version: str, ssh_username=None
432 455 ):
433 456 """Publish Windows release artifacts to mercurial-scm.org."""
434 457 all_paths = resolve_all_artifacts(dist_path, version)
435 458
436 459 for p in all_paths:
437 460 if not p.exists():
438 461 raise Exception('%s not found' % p)
439 462
440 463 client = paramiko.SSHClient()
441 464 client.load_system_host_keys()
442 465 # We assume the system SSH configuration knows how to connect.
443 466 print('connecting to mercurial-scm.org via ssh...')
444 467 try:
445 468 client.connect('mercurial-scm.org', username=ssh_username)
446 469 except paramiko.AuthenticationException:
447 470 print('error authenticating; is an SSH key available in an SSH agent?')
448 471 raise
449 472
450 473 print('SSH connection established')
451 474
452 475 print('opening SFTP client...')
453 476 sftp = client.open_sftp()
454 477 print('SFTP client obtained')
455 478
456 479 for p in all_paths:
457 480 dest_path = '/var/www/release/windows/%s' % p.name
458 481 print('uploading %s to %s' % (p, dest_path))
459 482
460 483 with p.open('rb') as fh:
461 484 data = fh.read()
462 485
463 486 with sftp.open(dest_path, 'wb') as fh:
464 487 fh.write(data)
465 488 fh.chmod(0o0664)
466 489
467 490 latest_dat_path = '/var/www/release/windows/latest.dat'
468 491
469 492 now = datetime.datetime.utcnow()
470 493 backup_path = dist_path / (
471 494 'latest-windows-%s.dat' % now.strftime('%Y%m%dT%H%M%S')
472 495 )
473 496 print('backing up %s to %s' % (latest_dat_path, backup_path))
474 497
475 498 with sftp.open(latest_dat_path, 'rb') as fh:
476 499 latest_dat_old = fh.read()
477 500
478 501 with backup_path.open('wb') as fh:
479 502 fh.write(latest_dat_old)
480 503
481 504 print('writing %s with content:' % latest_dat_path)
482 505 latest_dat_content = generate_latest_dat(version)
483 506 print(latest_dat_content)
484 507
485 508 with sftp.open(latest_dat_path, 'wb') as fh:
486 509 fh.write(latest_dat_content.encode('ascii'))
487 510
488 511
489 512 def publish_artifacts(
490 513 dist_path: pathlib.Path,
491 514 version: str,
492 515 pypi=True,
493 516 mercurial_scm_org=True,
494 517 ssh_username=None,
495 518 ):
496 519 """Publish Windows release artifacts.
497 520
498 521 Files are found in `dist_path`. We will look for files with version string
499 522 `version`.
500 523
501 524 `pypi` controls whether we upload to PyPI.
502 525 `mercurial_scm_org` controls whether we upload to mercurial-scm.org.
503 526 """
504 527 if pypi:
505 528 publish_artifacts_pypi(dist_path, version)
506 529
507 530 if mercurial_scm_org:
508 531 publish_artifacts_mercurial_scm_org(
509 532 dist_path, version, ssh_username=ssh_username
510 533 )
General Comments 0
You need to be logged in to leave comments. Login now