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