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